Inhaltsverzeichnis
Einleitung
Kapitel 1
Versionen des Betriebssystems Windows
Windows 10 und zukünftige Windows-Versionen
Windows 10 und OneCore
Grundprinzipien und -begriffe
Die Windows-API
Dienste, Funktionen und Routinen
Prozesse
Threads
Jobs
Virtueller Arbeitsspeicher
Kernel- und Benutzermodus
Hypervisor
Firmware
Terminaldienste und mehrere Sitzungen
Objekte und Handles
Sicherheit
Die Registrierung
Unicode
Die internen Mechanismen von Windows untersuchen
Leistungsüberwachung und Ressourcenmonitor
Kernel-Debugging
Windows Software Development Kit
Windows Driver Kit
Sysinternals-Werkzeuge
Zusammenfassung
Kapitel 2
Anforderungen und Designziele
Das Modell des Betriebssystems
Die Architektur im Überblick
Portierbarkeit
Symmetrisches Multiprocessing
Skalierbarkeit
Unterschiede zwischen den Client- und Serverversionen
Testbuild
Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick
Hauptsystemkomponenten
Umgebungsteilsysteme und Teilsystem-DLLs
Weitere Teilsysteme
Exekutive
Der Kernel
Die Hardwareabstraktionsschicht
Gerätetreiber
Systemprozesse
Zusammenfassung
Kapitel 3
Prozesse erstellen
Argumente der CreateProcess*-Funktionen
Moderne Windows-Prozesse erstellen
Andere Arten von Prozessen erstellen
Interne Mechanismen von Prozessen
Geschützte Prozesse
Protected Process Light (PPL)
Unterstützung für Drittanbieter-PPLs
Minimale und Pico-Prozesse
Minimale Prozesse
Pico-Prozesse
Trustlets (sichere Prozesse)
Der Aufbau von Trustlets
Richtlinien-Metadaten für Trustlets
Attribute von Trustlets
Integrierte System-Trustlets
Trustlet-Identität
IUM-Dienste
Für Trustlets zugängliche Systemaufrufe
Der Ablauf von CreateProcess
Phase 1
Phase 2
Phase 3
Phase 4
Phase 5
Phase 6
Phase 7
Einen Prozess beenden
Der Abbildlader
Frühe Prozessinitialisierung
DLL-Namensauflösung und -Umleitung
Die Datenbank der geladenen Module
Importanalyse
Prozessinitialisierung nach dem Import
SwitchBack
API-Sets
Jobs
Grenzwerte für Jobs
Umgang mit Jobs
Verschachtelte Jobs
Windows-Container (Serversilos)
Zusammenfassung
Kapitel 4
Threads erstellen
Interne Strukturen von Threads
Datenstrukturen
Geburt eines Threads
Die Threadaktivität untersuchen
Einschränkungen für Threads in geschützten Prozessen
Threadplanung
Überblick über die Threadplanung in Windows
Prioritätsstufen
Threadstatus
Die Dispatcherdatenbank
Das Quantum
Prioritätserhöhung
Kontextwechsel
Mögliche Fälle bei der Threadplanung
Leerlaufthreads
Anhalten von Threads
Einfrieren und Tiefgefrieren
Threadauswahl
Mehrprozessorsysteme
Threadauswahl auf Mehrprozessorsystemen
Prozessorauswahl
Heterogene Threadplanung (big.LITTLE)
Gruppengestützte Threadplanung
Dynamische gleichmäßige Planung (DFSS)
Grenzwerte für die CPU-Rate
Dynamisches Hinzufügen und Ersetzen von Prozessoren
Arbeitsfactories (Threadpools)
Erstellen von Arbeitsfactories
Zusammenfassung
Kapitel 5
Einführung in den Speicher-Manager
Komponenten des Speicher-Managers
Große und kleine Seiten
Die Speichernutzung untersuchen
Interne Synchronisierung
Vom Speicher-Manager bereitgestellte Dienste
Seitenstatus und Speicherzuweisungen
Gesamter zugesicherter Speicher und Zusicherungsgrenzwert
Seiten im Arbeitsspeicher festhalten
Granularität der Zuweisung
Gemeinsam genutzter Arbeitsspeicher und zugeordnete Dateien
Speicherschutz
Datenausführungsverhinderung
Kopieren beim Schreiben
AWE (Address Windowing Extensions)
Kernelmodusheaps (Systemspeicherpools)
Poolgrößen
Die Poolnutzung überwachen
Look-Aside-Listen
Der Heap-Manager
Prozessheaps
Arten von Heaps
NT-Heaps
Heapsynchronisierung
Der Low-Fragmentation-Heap
Segmentheaps
Sicherheitseinrichtungen von Heaps
Debugging einrichten für Heaps
Der Seitenheap
Der fehlertolerante Heap
Layouts für virtuelle Adressräume
x86-Adressraumlayouts
Das Layout des x86-Systemadressraums
x86-Sitzungsraum
System-Seitentabelleneinträge
ARM-Adressraumlayout
64-Bit-Adressraumlayout
Einschränkungen bei der virtuellen Adressierung auf x64-Systemen
Dynamische Verwaltung des virtuellen Systemadressraums
Kontingente für den virtuellen Systemadressraum
Layout des Benutzeradressraums
Adressübersetzung
Übersetzung virtueller Adressen auf x86-Systemen
Der Look-Aside-Übersetzungspuffer für die Übersetzung
Übersetzung virtueller Adressen auf x64-Systemen
Übersetzung virtueller Adressen auf ARM-Systemen
Seitenfehler
Ungültige PTEs
Prototyp-PTEs
Einlagerungs-E/A
Seitenfehlerkollisionen
Seitencluster
Auslagerungsdateien
Gesamter zugesicherter Speicher und systemweiter Zusicherungsgrenzwert
Der Zusammenhang zwischen dem gesamten zugesicherten Speicher und der Größe der Auslagerungsdatei
Stacks
Benutzerstacks
Kernelstacks
Der DPC-Stack
VADs
Prozess-VADs
Umlauf-VADs
NUMA
Abschnittsobjekte
Arbeitssätze
Auslagerung bei Bedarf
Der logische Prefetcher und ReadyBoot
Platzierungsrichtlinien
Verwaltung von Arbeitssätzen
Der Balance-Set-Manager und der Swapper
Systemarbeitssätze
Speicherbenachrichtigungsereignisse
Die PFN-Datenbank
Seitenlistendynamik
Seitenpriorität
Die Schreibthreads für geänderte und für zugeordnete Seiten
PFN-Datenstrukturen
Reservierungen in der Auslagerungsdatei
Grenzwerte für den physischen Speicher
Speichergrenzwerte für Windows-Clienteditionen
Speicherkomprimierung
Ablauf der Komprimierung
Komprimierungsarchitektur
Speicherpartitionen
Speicherzusammenführung
Die Suchphase
Die Klassifizierungsphase
Die Zusammenführungsphase
Vom privaten zum gemeinsamen PTE
Freigabe von zusammengeführten Seiten
Speicherenklaven
Programmierschnittstellen
Initialisierung von Speicherenklaven
Aufbau von Enklaven
Daten in eine Enklave laden
Eine Enklave initialisieren
Vorausschauende Speicherverwaltung (SuperFetch)
Komponenten
Ablaufverfolgung und Protokollierung
Szenarien
Seitenpriorität und Rebalancing
Leistungsstabilisierung
ReadyBoost
ReadyDrive
Prozessreflexion
Zusammenfassung
Kapitel 6
Komponenten des E/A-Systems
Der E/A-Manager
Typische E/A-Verarbeitung
IRQ-Ebenen und verzögerte Prozeduraufrufe
IRQ-Ebenen
Verzögerte Prozeduraufrufe
Gerätetreiber
Arten von Gerätetreibern
Aufbau eines Treibers
Treiber- und Geräteobjekte
Geräte öffnen
E/A-Verarbeitung
Verschiedene Arten der E/A
E/A-Anforderungspakete
E/A-Anforderungen an einen einschichtigen Hardwaretreiber
E/A-Anforderungen an geschichtete Treiber
Threadagnostische E/A
Abbrechen der E/A
E/A-Vervollständigungsports
E/A-Priorisierung
Containerbenachrichtigungen
Treiberüberprüfung
E/A-Überprüfungsoptionen
Speicherüberprüfungsoptionen
Der PnP-Manager
Der Grad der Unterstützung für Plug & Play
Geräteauflistung
Gerätestacks
Treiberunterstützung für Plug & Play
Installation von Plug-&-Play-Treibern
Laden und Installieren von Treibern
Treiber laden
Treiberinstallation
Windows Driver Foundation
Kernel-Mode Driver Framework
Das E/A-Modell von KMDF
User-Mode Driver Framework
Energieverwaltung
Verbundener und moderner Standbymodus
Funktionsweise der Energieverwaltung
Energieverwaltung durch die Treiber
Steuerung der Geräteenergiezustände durch den Treiber und die Anwendung
Das Framework für die Energieverwaltung
Energieverfügbarkeitsanforderungen
Zusammenfassung
Kapitel 7
Sicherheitseinstufungen
Trusted Computer System Evaluation Criteria
Common Criteria
Systemkomponenten für die Sicherheit
Virtualisierungsgestützte Sicherheit
Credential Guard
Device Guard
Objekte schützen
Zugriffsprüfungen
Sicherheitskennungen
Virtuelle Dienstkonten
Sicherheitsdeskriptoren und Zugriffssteuerung
Dynamische Zugriffssteuerung
Die AuthZ-API
Bedingte Zugriffssteuerungseinträge
Privilegien und Kontorechte
Kontorechte
Privilegien
Superprivilegien
Zugriffstokens von Prozessen und Threads
Sicherheitsüberwachung
Überwachung des Objektzugriffs
Globale Überwachungsrichtlinie
Erweiterte Überwachungsrichtlinienkonfiguration
Anwendungscontainer
UWP-Apps im Überblick
Anwendungscontainer
Anmeldung
Initialisierung durch Winlogon
Die einzelnen Schritte der Benutzeranmeldung
Sichere Authentifizierung
Das Windows-Biometrieframework
Windows Hello
Benutzerkontensteuerung und Virtualisierung
Virtualisierung des Dateisystems und der Registrierung
Rechteerhöhung
Schutz gegen Exploits
Abwehrmaßnahmen auf Prozessebene
Control Flow Integrity
Zusicherungen
Anwendungsidentifizierung
AppLocker
Richtlinien für Softwareeinschränkung
Kernelpatchschutz
PatchGuard
HyperGuard
Zusammenfassung
Index

Автор: Yosifovich P.   Ionescu A.   Russinovich M.E.   Solomon D.A.  

Теги: windows   informatik   betriebssysteme  

ISBN: 978-3-86490-538-4

Год: 2018

Текст
                    
Zu diesem Buch – sowie zu vielen weiteren dpunkt.büchern – können Sie auch das entsprechende E-Book im PDF-Format herunterladen. Werden Sie dazu einfach Mitglied bei dpunkt.plus+: www.dpunkt.plus
Für meine Familie: meine Frau Idit und unsere Kinder Danielle, Amit und Yoav. Danke für eure Geduld und eure Ermutigung während dieser anspruchsvollen Arbeit. – Pavel Yosifovich Für meine Eltern, die mich angeleitet und angeregt haben, meine Träume zu verfolgen, und für meine Familie, die während all dieser zahllosen Abende hinter mir gestanden hat. – Alex Ionescu Für unsere Eltern, die uns angeleitet und angeregt haben, unsere Träume zu verfolgen. – Mark E. Russinovich und David A. Solomon

Windows Internals Band 1: Systemarchitektur, Prozesse, Threads, Speicherverwaltung, Sicherheit und mehr Übersetzung der 7. englischsprachigen Auflage Pavel Yosifovich Alex Ionescu Mark E. Russinovich David A. Solomon
Pavel Yosifovich Alex Ionescu Mark E. Russinovich David A. Solomon Lektorat: Sandra Bollenbacher Übersetzung: G&U Language & Publishing Services GmbH, www.gundu.com Copy-Editing: Petra Heubach-Erdmann Satz: G&U Language & Publishing Services GmbH, www.gundu.com Umschlaggestaltung: Helmut Kraus, exclam.de Druckerei: C.H.Beck ISBN: Print PDF ePub mobi 978-3-86490-538-4 978-3-96088-338-8 978-3-96088-339-5 978-3-96088-340-1 Translation Copyright für die deutschsprachige Ausgabe © 2018 dpunkt.verlag GmbH Wieblinger Weg 17 69123 Heidelberg Authorized translation from the English language edition, entitled WINDOWS INTERNALS, PART 1: SYSTEM ARCHITECTURE, PROCESSES, THREADS, MEMORY MANAGEMENT, AND MORE, 7th Edition by PAVEL YOSIFOVICH, ALEX IONESCU, MARK RUSSINOVICH, DAVID SOLOMON, published by Pearson Education, Inc, publishing as Microsoft Press, Copyright © 2017 by Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich and David A. Solomon All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. German language edition published by dpunkt.verlag GmbH, Copyright © 2018 ISBN of the English language edition: 978-0-7356-8418-8 Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen. Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen. Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buchs stehen. 543210
Inhaltsverzeichnis Einleitung  xvii Kapitel 1 Begriffe und Werkzeuge  1 Versionen des Betriebssystems Windows ����������������������������������������������������������������������� 1 Windows 10 und zukünftige Windows-Versionen ������������������������������������������������� 3 Windows 10 und OneCore ����������������������������������������������������������������������������������������� 3 Grundprinzipien und -begriffe ����������������������������������������������������������������������������������������� 4 Die Windows-API ��������������������������������������������������������������������������������������������������������� 4 Dienste, Funktionen und Routinen ��������������������������������������������������������������������������� 7 Prozesse ������������������������������������������������������������������������������������������������������������������������ 8 Threads ����������������������������������������������������������������������������������������������������������������������� 20 Jobs ����������������������������������������������������������������������������������������������������������������������������� 23 Virtueller Arbeitsspeicher ����������������������������������������������������������������������������������������� 23 Kernel- und Benutzermodus ����������������������������������������������������������������������������������� 26 Hypervisor ������������������������������������������������������������������������������������������������������������������ 30 Firmware ��������������������������������������������������������������������������������������������������������������������� 32 Terminaldienste und mehrere Sitzungen ��������������������������������������������������������������� 32 Objekte und Handles ����������������������������������������������������������������������������������������������� 33 Sicherheit ������������������������������������������������������������������������������������������������������������������� 34 Die Registrierung ������������������������������������������������������������������������������������������������������� 36 Unicode ����������������������������������������������������������������������������������������������������������������������� 37 Die internen Mechanismen von Windows untersuchen ��������������������������������������������� 39 Leistungsüberwachung und Ressourcenmonitor ������������������������������������������������� 40 Kernel-Debugging ����������������������������������������������������������������������������������������������������� 42 Windows Software Development Kit ��������������������������������������������������������������������� 48 Windows Driver Kit ��������������������������������������������������������������������������������������������������� 49 Sysinternals-Werkzeuge ������������������������������������������������������������������������������������������� 49 Zusammenfassung ����������������������������������������������������������������������������������������������������������� 50 vii
Kapitel 2 Systemarchitektur  51 Anforderungen und Designziele ����������������������������������������������������������������������������������� 51 Das Modell des Betriebssystems ������������������������������������������������������������������������������������� 52 Die Architektur im Überblick ����������������������������������������������������������������������������������������� 53 Portierbarkeit ������������������������������������������������������������������������������������������������������������� 56 Symmetrisches Multiprocessing ����������������������������������������������������������������������������� 57 Skalierbarkeit ������������������������������������������������������������������������������������������������������������� 60 Unterschiede zwischen den Client- und Serverversionen ����������������������������������� 61 Testbuild ��������������������������������������������������������������������������������������������������������������������� 64 Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick ��������������������������� 66 Hauptsystemkomponenten ��������������������������������������������������������������������������������������������� 69 Umgebungsteilsysteme und Teilsystem-DLLs ������������������������������������������������������� 70 Weitere Teilsysteme ��������������������������������������������������������������������������������������������������� 76 Exekutive ��������������������������������������������������������������������������������������������������������������������� 81 Der Kernel ������������������������������������������������������������������������������������������������������������������� 84 Die Hardwareabstraktionsschicht ��������������������������������������������������������������������������� 88 Gerätetreiber ������������������������������������������������������������������������������������������������������������� 92 Systemprozesse ��������������������������������������������������������������������������������������������������������� 98 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 111 Kapitel 3 Prozesse und Jobs  113 Prozesse erstellen ����������������������������������������������������������������������������������������������������������� 113 Argumente der CreateProcess*-Funktionen ������������������������������������������������������� 114 Moderne Windows-Prozesse erstellen ����������������������������������������������������������������� 115 Andere Arten von Prozessen erstellen ����������������������������������������������������������������� 116 Interne Mechanismen von Prozessen ������������������������������������������������������������������������� 117 Geschützte Prozesse ������������������������������������������������������������������������������������������������������� 126 Protected Process Light (PPL) ������������������������������������������������������������������������������� 128 Unterstützung für Drittanbieter-PPLs ������������������������������������������������������������������� 133 Minimale und Pico-Prozesse ����������������������������������������������������������������������������������������� 134 Minimale Prozesse ��������������������������������������������������������������������������������������������������� 134 Pico-Prozesse ����������������������������������������������������������������������������������������������������������� 134 viii Inhaltsverzeichnis
Trustlets (sichere Prozesse) ������������������������������������������������������������������������������������������� 137 Der Aufbau von Trustlets ��������������������������������������������������������������������������������������� 137 Richtlinien-Metadaten für Trustlets ��������������������������������������������������������������������� 138 Attribute von Trustlets �������������������������������������������������������������������������������������������� 139 Integrierte System-Trustlets ����������������������������������������������������������������������������������� 140 Trustlet-Identität ����������������������������������������������������������������������������������������������������� 140 IUM-Dienste ������������������������������������������������������������������������������������������������������������� 141 Für Trustlets zugängliche Systemaufrufe ������������������������������������������������������������� 142 Der Ablauf von CreateProcess ��������������������������������������������������������������������������������������� 144 Phase 1: Parameter und Flags konvertieren und validieren ����������������������������� 145 Phase 2: Das auszuführende Abbild öffnen ��������������������������������������������������������� 151 Phase 3: Das Windows-Exekutivprozessobjekt erstellen ����������������������������������� 154 Phase 4: Den ursprünglichen Thread mit seinem Stack und Kontext erstellen ����������������������������������������������������������������������������������������������������� 160 Phase 5: Spezifische Prozessinitialisierung für das Windows-Teilsystem durchführen ��������������������������������������������������������������������������������������������� 162 Phase 6: Ausführung des ursprünglichen Threads starten ������������������������������� 164 Phase 7: Prozessinitialisierung im Kontext des neuen Prozesses durchführen ��������������������������������������������������������������������������������������������� 165 Einen Prozess beenden ������������������������������������������������������������������������������������������������� 172 Der Abbildlader ��������������������������������������������������������������������������������������������������������������� 173 Frühe Prozessinitialisierung ����������������������������������������������������������������������������������� 176 DLL-Namensauflösung und -Umleitung ������������������������������������������������������������� 179 Die Datenbank der geladenen Module ��������������������������������������������������������������� 183 Importanalyse ��������������������������������������������������������������������������������������������������������� 188 Prozessinitialisierung nach dem Import ��������������������������������������������������������������� 190 SwitchBack ��������������������������������������������������������������������������������������������������������������� 191 API-Sets ��������������������������������������������������������������������������������������������������������������������� 194 Jobs ����������������������������������������������������������������������������������������������������������������������������������� 196 Grenzwerte für Jobs ����������������������������������������������������������������������������������������������� 197 Umgang mit Jobs ��������������������������������������������������������������������������������������������������� 199 Verschachtelte Jobs ������������������������������������������������������������������������������������������������� 199 Windows-Container (Serversilos) ������������������������������������������������������������������������� 204 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 213 Inhaltsverzeichnis ix
Kapitel 4 Threads  215 Threads erstellen ������������������������������������������������������������������������������������������������������������� 215 Interne Strukturen von Threads ����������������������������������������������������������������������������������� 216 Datenstrukturen ������������������������������������������������������������������������������������������������������� 216 Geburt eines Threads ��������������������������������������������������������������������������������������������� 230 Die Threadaktivität untersuchen ��������������������������������������������������������������������������������� 231 Einschränkungen für Threads in geschützten Prozessen ����������������������������������� 237 Threadplanung ��������������������������������������������������������������������������������������������������������������� 238 Überblick über die Threadplanung in Windows ������������������������������������������������� 239 Prioritätsstufen ��������������������������������������������������������������������������������������������������������� 240 Threadstatus ������������������������������������������������������������������������������������������������������������� 248 Die Dispatcherdatenbank ��������������������������������������������������������������������������������������� 255 Das Quantum ����������������������������������������������������������������������������������������������������������� 258 Prioritätserhöhung ������������������������������������������������������������������������������������������������� 266 Kontextwechsel ������������������������������������������������������������������������������������������������������� 286 Mögliche Fälle bei der Threadplanung ��������������������������������������������������������������� 288 Leerlaufthreads ������������������������������������������������������������������������������������������������������� 292 Anhalten von Threads ��������������������������������������������������������������������������������������������� 296 Einfrieren und Tiefgefrieren ����������������������������������������������������������������������������������� 296 Threadauswahl ��������������������������������������������������������������������������������������������������������� 299 Mehrprozessorsysteme ������������������������������������������������������������������������������������������� 301 Threadauswahl auf Mehrprozessorsystemen ����������������������������������������������������� 319 Prozessorauswahl ����������������������������������������������������������������������������������������������������� 320 Heterogene Threadplanung (big.LITTLE) ������������������������������������������������������������� 322 Gruppengestützte Threadplanung ������������������������������������������������������������������������������� 324 Dynamische gleichmäßige Planung (DFSS) ��������������������������������������������������������� 326 Grenzwerte für die CPU-Rate ������������������������������������������������������������������������������� 330 Dynamisches Hinzufügen und Ersetzen von Prozessoren ��������������������������������� 333 Arbeitsfactories (Threadpools) ������������������������������������������������������������������������������������� 335 Erstellen von Arbeitsfactories �������������������������������������������������������������������������������� 337 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 339 x Inhaltsverzeichnis
Kapitel 5 Speicherverwaltung  341 Einführung in den Speicher-Manager ������������������������������������������������������������������������� 341 Komponenten des Speicher-Managers ��������������������������������������������������������������� 342 Große und kleine Seiten ����������������������������������������������������������������������������������������� 343 Die Speichernutzung untersuchen ����������������������������������������������������������������������� 345 Interne Synchronisierung ��������������������������������������������������������������������������������������� 350 Vom Speicher-Manager bereitgestellte Dienste ������������������������������������������������������� 350 Seitenstatus und Speicherzuweisungen ��������������������������������������������������������������� 352 Gesamter zugesicherter Speicher und Zusicherungsgrenzwert ����������������������� 356 Seiten im Arbeitsspeicher festhalten ������������������������������������������������������������������� 356 Granularität der Zuweisung ����������������������������������������������������������������������������������� 357 Gemeinsam genutzter Arbeitsspeicher und zugeordnete Dateien ����������������� 357 Speicherschutz ��������������������������������������������������������������������������������������������������������� 360 Datenausführungsverhinderung ��������������������������������������������������������������������������� 362 Kopieren beim Schreiben ��������������������������������������������������������������������������������������� 366 AWE (Address Windowing Extensions) ����������������������������������������������������������������� 367 Kernelmodusheaps (Systemspeicherpools) ����������������������������������������������������������������� 370 Poolgrößen ��������������������������������������������������������������������������������������������������������������� 370 Die Poolnutzung überwachen ������������������������������������������������������������������������������� 372 Look-Aside-Listen ��������������������������������������������������������������������������������������������������� 377 Der Heap-Manager ��������������������������������������������������������������������������������������������������������� 378 Prozessheaps ����������������������������������������������������������������������������������������������������������� 379 Arten von Heaps ����������������������������������������������������������������������������������������������������� 380 NT-Heaps ����������������������������������������������������������������������������������������������������������������� 380 Heapsynchronisierung ������������������������������������������������������������������������������������������� 381 Der Low-Fragmentation-Heap ����������������������������������������������������������������������������� 381 Segmentheaps ��������������������������������������������������������������������������������������������������������� 383 Sicherheitseinrichtungen von Heaps ������������������������������������������������������������������� 388 Debugging einrichten für Heaps ��������������������������������������������������������������������������� 390 Der Seitenheap ��������������������������������������������������������������������������������������������������������� 391 Der fehlertolerante Heap ��������������������������������������������������������������������������������������� 394 Layouts für virtuelle Adressräume ������������������������������������������������������������������������������� 396 x86-Adressraumlayouts ����������������������������������������������������������������������������������������� 397 Das Layout des x86-Systemadressraums ������������������������������������������������������������� 401 Inhaltsverzeichnis xi
x86-Sitzungsraum ��������������������������������������������������������������������������������������������������� 401 System-Seitentabelleneinträge ����������������������������������������������������������������������������� 404 ARM-Adressraumlayout ����������������������������������������������������������������������������������������� 405 64-Bit-Adressraumlayout ��������������������������������������������������������������������������������������� 406 Einschränkungen bei der virtuellen Adressierung auf x64-Systemen ������������� 408 Dynamische Verwaltung des virtuellen Systemadressraums ��������������������������� 408 Kontingente für den virtuellen Systemadressraum ������������������������������������������� 415 Layout des Benutzeradressraums ������������������������������������������������������������������������� 416 Adressübersetzung ��������������������������������������������������������������������������������������������������������� 422 Übersetzung virtueller Adressen auf x86-Systemen ����������������������������������������� 422 Der Look-Aside-Übersetzungspuffer für die Übersetzung ������������������������������� 429 Übersetzung virtueller Adressen auf x64-Systemen ����������������������������������������� 433 Übersetzung virtueller Adressen auf ARM-Systemen ��������������������������������������� 434 Seitenfehler ��������������������������������������������������������������������������������������������������������������������� 435 Ungültige PTEs ��������������������������������������������������������������������������������������������������������� 436 Prototyp-PTEs ����������������������������������������������������������������������������������������������������������� 438 Einlagerungs-E/A ����������������������������������������������������������������������������������������������������� 440 Seitenfehlerkollisionen ������������������������������������������������������������������������������������������� 440 Seitencluster ������������������������������������������������������������������������������������������������������������� 441 Auslagerungsdateien ��������������������������������������������������������������������������������������������� 442 Gesamter zugesicherter Speicher und systemweiter Zusicherungsgrenzwert ����������������������������������������������������������������������������������������� 448 Der Zusammenhang zwischen dem gesamten zugesicherten Speicher und der Größe der Auslagerungsdatei ����������������������������������������������������������������� 452 Stacks ������������������������������������������������������������������������������������������������������������������������������� 454 Benutzerstacks ��������������������������������������������������������������������������������������������������������� 455 Kernelstacks ������������������������������������������������������������������������������������������������������������� 456 Der DPC-Stack ��������������������������������������������������������������������������������������������������������� 457 VADs ��������������������������������������������������������������������������������������������������������������������������������� 457 Prozess-VADs ����������������������������������������������������������������������������������������������������������� 458 Umlauf-VADs ����������������������������������������������������������������������������������������������������������� 460 NUMA ������������������������������������������������������������������������������������������������������������������������������� 461 Abschnittsobjekte ����������������������������������������������������������������������������������������������������������� 462 Arbeitssätze ��������������������������������������������������������������������������������������������������������������������� 472 Auslagerung bei Bedarf ����������������������������������������������������������������������������������������� 472 Der logische Prefetcher und ReadyBoot ������������������������������������������������������������� 473 xii Inhaltsverzeichnis
Platzierungsrichtlinien ������������������������������������������������������������������������������������������� 477 Verwaltung von Arbeitssätzen ������������������������������������������������������������������������������� 477 Der Balance-Set-Manager und der Swapper ������������������������������������������������������ 482 Systemarbeitssätze ������������������������������������������������������������������������������������������������� 483 Speicherbenachrichtigungsereignisse ����������������������������������������������������������������� 485 Die PFN-Datenbank ������������������������������������������������������������������������������������������������������� 487 Seitenlistendynamik ����������������������������������������������������������������������������������������������� 491 Seitenpriorität ���������������������������������������������������������������������������������������������������������� 499 Die Schreibthreads für geänderte und für zugeordnete Seiten ����������������������� 502 PFN-Datenstrukturen ��������������������������������������������������������������������������������������������� 504 Reservierungen in der Auslagerungsdatei ����������������������������������������������������������� 509 Grenzwerte für den physischen Speicher ������������������������������������������������������������������� 512 Speichergrenzwerte für Windows-Clienteditionen ������������������������������������������� 513 Speicherkomprimierung ����������������������������������������������������������������������������������������������� 515 Ablauf der Komprimierung ����������������������������������������������������������������������������������� 516 Komprimierungsarchitektur ����������������������������������������������������������������������������������� 520 Speicherpartitionen ������������������������������������������������������������������������������������������������������� 523 Speicherzusammenführung ����������������������������������������������������������������������������������������� 526 Die Suchphase ��������������������������������������������������������������������������������������������������������� 528 Die Klassifizierungsphase ��������������������������������������������������������������������������������������� 529 Die Zusammenführungsphase ������������������������������������������������������������������������������� 530 Vom privaten zum gemeinsamen PTE ����������������������������������������������������������������� 530 Freigabe von zusammengeführten Seiten ����������������������������������������������������������� 533 Speicherenklaven ����������������������������������������������������������������������������������������������������������� 536 Programmierschnittstellen ������������������������������������������������������������������������������������� 538 Initialisierung von Speicherenklaven ������������������������������������������������������������������� 538 Aufbau von Enklaven ��������������������������������������������������������������������������������������������� 539 Daten in eine Enklave laden ���������������������������������������������������������������������������������� 541 Eine Enklave initialisieren ��������������������������������������������������������������������������������������� 542 Vorausschauende Speicherverwaltung (SuperFetch) ����������������������������������������������� 542 Komponenten ��������������������������������������������������������������������������������������������������������� 543 Ablaufverfolgung und Protokollierung ��������������������������������������������������������������� 544 Szenarien ������������������������������������������������������������������������������������������������������������������� 545 Seitenpriorität und Rebalancing ��������������������������������������������������������������������������� 546 Leistungsstabilisierung ������������������������������������������������������������������������������������������� 549 Inhaltsverzeichnis xiii
ReadyBoost ��������������������������������������������������������������������������������������������������������������� 550 ReadyDrive ��������������������������������������������������������������������������������������������������������������� 551 Prozessreflexion ������������������������������������������������������������������������������������������������������� 552 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 554 Kapitel 6 Das E/A-System  555 Komponenten des E/A-Systems ����������������������������������������������������������������������������������� 555 Der E/A-Manager ��������������������������������������������������������������������������������������������������� 557 Typische E/A-Verarbeitung ������������������������������������������������������������������������������������ 558 IRQ-Ebenen und verzögerte Prozeduraufrufe ����������������������������������������������������������� 560 IRQ-Ebenen ������������������������������������������������������������������������������������������������������������� 561 Verzögerte Prozeduraufrufe ��������������������������������������������������������������������������������� 563 Gerätetreiber ������������������������������������������������������������������������������������������������������������������� 564 Arten von Gerätetreibern ��������������������������������������������������������������������������������������� 565 Aufbau eines Treibers ��������������������������������������������������������������������������������������������� 572 Treiber- und Geräteobjekte ����������������������������������������������������������������������������������� 574 Geräte öffnen ����������������������������������������������������������������������������������������������������������� 582 E/A-Verarbeitung ����������������������������������������������������������������������������������������������������������� 586 Verschiedene Arten der E/A ����������������������������������������������������������������������������������� 586 E/A-Anforderungspakete ��������������������������������������������������������������������������������������� 590 E/A-Anforderungen an einen einschichtigen Hardwaretreiber ����������������������� 603 E/A-Anforderungen an geschichtete Treiber ����������������������������������������������������� 613 Threadagnostische E/A ������������������������������������������������������������������������������������������� 616 Abbrechen der E/A ������������������������������������������������������������������������������������������������� 617 E/A-Vervollständigungsports ��������������������������������������������������������������������������������� 622 E/A-Priorisierung ����������������������������������������������������������������������������������������������������� 627 Containerbenachrichtigungen ������������������������������������������������������������������������������� 634 Treiberüberprüfung ������������������������������������������������������������������������������������������������������� 635 E/A-Überprüfungsoptionen ����������������������������������������������������������������������������������� 638 Speicherüberprüfungsoptionen ��������������������������������������������������������������������������� 638 Der PnP-Manager ����������������������������������������������������������������������������������������������������������� 643 Der Grad der Unterstützung für Plug & Play ����������������������������������������������������� 644 Geräteauflistung ����������������������������������������������������������������������������������������������������� 645 Gerätestacks ������������������������������������������������������������������������������������������������������������� 649 xiv Inhaltsverzeichnis
Treiberunterstützung für Plug & Play ������������������������������������������������������������������� 655 Installation von Plug-&-Play-Treibern ����������������������������������������������������������������� 656 Laden und Installieren von Treibern ��������������������������������������������������������������������������� 661 Treiber laden ������������������������������������������������������������������������������������������������������������� 661 Treiberinstallation ��������������������������������������������������������������������������������������������������� 663 Windows Driver Foundation ����������������������������������������������������������������������������������������� 664 Kernel-Mode Driver Framework ��������������������������������������������������������������������������� 665 Das E/A-Modell von KMDF ����������������������������������������������������������������������������������� 672 User-Mode Driver Framework ������������������������������������������������������������������������������� 675 Energieverwaltung ��������������������������������������������������������������������������������������������������������� 679 Verbundener und moderner Standbymodus ����������������������������������������������������� 683 Funktionsweise der Energieverwaltung ��������������������������������������������������������������� 684 Energieverwaltung durch die Treiber ������������������������������������������������������������������� 686 Steuerung der Geräteenergiezustände durch den Treiber und die Anwendung ������������������������������������������������������������������������������������������������������������� 689 Das Framework für die Energieverwaltung ��������������������������������������������������������� 690 Energieverfügbarkeitsanforderungen ����������������������������������������������������������������� 692 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 694 Kapitel 7 Sicherheit  697 Sicherheitseinstufungen ����������������������������������������������������������������������������������������������� 697 Trusted Computer System Evaluation Criteria ����������������������������������������������������� 697 Common Criteria ����������������������������������������������������������������������������������������������������� 699 Systemkomponenten für die Sicherheit ��������������������������������������������������������������������� 700 Virtualisierungsgestützte Sicherheit ��������������������������������������������������������������������������� 704 Credential Guard ����������������������������������������������������������������������������������������������������� 705 Device Guard ����������������������������������������������������������������������������������������������������������� 712 Objekte schützen ����������������������������������������������������������������������������������������������������������� 714 Zugriffsprüfungen ��������������������������������������������������������������������������������������������������� 716 Sicherheitskennungen ��������������������������������������������������������������������������������������������� 720 Virtuelle Dienstkonten ������������������������������������������������������������������������������������������� 745 Sicherheitsdeskriptoren und Zugriffssteuerung ������������������������������������������������� 751 Dynamische Zugriffssteuerung ����������������������������������������������������������������������������� 770 Inhaltsverzeichnis xv
Die AuthZ-API ����������������������������������������������������������������������������������������������������������������� 771 Bedingte Zugriffssteuerungseinträge ������������������������������������������������������������������� 772 Privilegien und Kontorechte ����������������������������������������������������������������������������������������� 773 Kontorechte ������������������������������������������������������������������������������������������������������������� 775 Privilegien ����������������������������������������������������������������������������������������������������������������� 776 Superprivilegien ������������������������������������������������������������������������������������������������������� 783 Zugriffstokens von Prozessen und Threads ��������������������������������������������������������������� 784 Sicherheitsüberwachung ����������������������������������������������������������������������������������������������� 785 Überwachung des Objektzugriffs ������������������������������������������������������������������������� 787 Globale Überwachungsrichtlinie ��������������������������������������������������������������������������� 790 Erweiterte Überwachungsrichtlinienkonfiguration ������������������������������������������� 792 Anwendungscontainer ��������������������������������������������������������������������������������������������������� 793 UWP-Apps im Überblick ����������������������������������������������������������������������������������������� 794 Anwendungscontainer ������������������������������������������������������������������������������������������� 796 Anmeldung ��������������������������������������������������������������������������������������������������������������������� 824 Initialisierung durch Winlogon ����������������������������������������������������������������������������� 826 Die einzelnen Schritte der Benutzeranmeldung ������������������������������������������������� 827 Sichere Authentifizierung ��������������������������������������������������������������������������������������� 833 Das Windows-Biometrieframework ��������������������������������������������������������������������� 835 Windows Hello ��������������������������������������������������������������������������������������������������������� 837 Benutzerkontensteuerung und Virtualisierung ��������������������������������������������������������� 838 Virtualisierung des Dateisystems und der Registrierung ����������������������������������� 839 Rechteerhöhung ����������������������������������������������������������������������������������������������������� 847 Schutz gegen Exploits ��������������������������������������������������������������������������������������������������� 855 Abwehrmaßnahmen auf Prozessebene ��������������������������������������������������������������� 856 Control Flow Integrity ��������������������������������������������������������������������������������������������� 861 Zusicherungen ��������������������������������������������������������������������������������������������������������� 875 Anwendungsidentifizierung ����������������������������������������������������������������������������������������� 880 AppLocker ����������������������������������������������������������������������������������������������������������������������� 882 Richtlinien für Softwareeinschränkung ����������������������������������������������������������������������� 887 Kernelpatchschutz ����������������������������������������������������������������������������������������������������������� 889 PatchGuard ��������������������������������������������������������������������������������������������������������������� 890 HyperGuard ������������������������������������������������������������������������������������������������������������� 894 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 896 Index  897 xvi Inhaltsverzeichnis
Einleitung Dieses Buch ist für Computerexperten (Entwickler, Sicherheitsforscher und Systemadministratoren) gedacht, die die Funktionsweise der Kernkomponenten von Microsoft Windows 10 und Windows Server 2016 kennenlernen möchten. Mit diesen Kenntnissen können Entwickler beim Schreiben von Anwendungen für die Windows-Plattform die Gründe für bestimmte Designentscheidungen besser verstehen. Sie helfen ihnen auch beim Debugging komplizierter Probleme. Auch Systemadministratoren profitieren von diesen Informationen, da das Wissen, wie das System in seinem Inneren »tickt«, sein Verhalten verständlicher macht und die Störungssuche erleichtert. Sicherheitsforscher erfahren hier, welche Fehlverhalten Softwareanwendungen und das Betriebssystem an den Tag legen können, wie sie missbraucht werden können und welche Schutzmaßnahmen und Sicherheitsfunktionen moderne Windows-Versionen dagegen bieten. Nach der Lektüre dieses Buches haben Sie ein besseres Verständnis darüber, wie Windows funktioniert und welche Gründe hinter seinem Verhalten stehen. Die Geschichte dieses Buches Dies ist die siebente Ausgabe eines Buches, das ursprünglich Inside Windows NT hieß (Microsoft Press, 1992) und noch vor der Erstveröffentlichung von Windows NT 3.1 von Helen Custer geschrieben wurde. Es war das erste Buch, das es je über Windows NT gab, und es bot wichtige Einsichten in die Architektur und das Design des Systems. Inside Windows NT, Second Edition (Microsoft Press, 1998) wurde von David Solomon geschrieben. Es deckte auch Windows NT 4.0 ab und ging weit tiefer in die technischen Details. Inside Windows 2000, Third Edition (Microsoft Press, 2000) wurde von David Solomon und Mark Russinovich geschrieben. Hier kamen viele neue Themen wie Starten und Herunterfahren, interne Mechanismen von Diensten und der Registrierung, Dateisystemtreiber und Netzwerkanbindung hinzu. Auch die Änderungen am Kernel in Windows 2000 wurden erläutert, darunter das Windows Driver Model (WDM), Plug & Play, Energieverwaltung, Windows-Verwaltungsinstrumentation (Windows Management Instrumentation, WMI), Verschlüsselung, Jobobjekte und Terminaldienste. Windows Internals, Fourth Edition (Microsoft Press, 2004) war die neue Ausgabe für Windows XP und Windows Server 2003 mit zusätzlichen Inhalten, um IT-Fachleuten zu helfen, ihre Kenntnisse über die internen Mechanismen von Windows praktisch anzuwenden, z. B. durch die Werkzeuge von Sysinternals und die Analyse von Absturzabbildern. Mit Windows Internals, Fifth Edition (Microsoft Press, 2009) wurde das Buch auf Windows Vista und Windows Server 2008 aktualisiert. Da Mark Russinovich eine Vollzeitstelle bei Microsoft annahm (wo er jetzt als CTO für Azure arbeitet), kam der neue Co-Autor Alex Ionescu hinzu. Zu den neuen Inhalten gehörten der Abbildlader, Einrichtungen für das Debugging im Benutzermodus, ALPC (Advanced Local Procedure Call) und Hyper-V. Die nächste Ausgabe, xvii
Windows Internals, Sixth Edition (Microsoft Press, 2012) war komplett aktualisiert, um viele Änderungen am Kernel in Windows 7 und Windows Server 2008 R2 abzudecken. Außerdem kamen viele neue praktische Experimente hinzu, um die Änderungen an den Werkzeugen deutlich zu machen. Änderungen in dieser Auflage Seit der letzten Aktualisierung dieses Buches hat Windows mehrere neue Releases durchlaufen und ist jetzt bei Windows 10 und Windows Server 2016 angekommen. Dabei ist Windows 10 jetzt praktisch der neue Name für Windows. Seit der Produktionsfreigabe hat es wiederum mehrere Releases gegeben. Bezeichnet werden sie jeweils mit einer vierstelligen Zahl, die sich aus dem Jahr und dem Monat der Veröffentlichung zusammensetzt, also z. B. Windows 10 Version 1703 für die Version vom März 2017. Zum Zeitpunkt der Abfassung dieses Buches hat Windows also seit Windows 7 mindestens sechs Versionen durchlaufen. Beginnend mit Windows 8 hat Microsoft mit einer Zusammenführung seiner Betriebssysteme begonnen, was für die Entwicklung und für das Windows-Konstruktionsteam von Vorteil ist. Windows 8 und Windows Phone 8 verwendeten bereits einen gemeinsamen Kernel, und mit Windows 8.1 und Windows Phone 8.1 kam auch eine Vereinheitlichung bei modernen Apps hinzu. Mit Windows 10 war die Zusammenführung abgeschlossen: Dieses Betriebssystem läuft auf Desktops, Laptops, Servern, der Xbox One, Smartphones (Windows Mobile 10), der HoloLens und verschiedenen IoT-Geräten (Internet of Things). Angesichts dieser großen Vereinheitlichung war die Zeit reif für eine neue Ausgabe dieses Buches, um die Änderungen von fast einem halben Jahrzehnt aufzuarbeiten, die zu einer stabileren Kernelarchitektur geführt haben. Daher deckt dieses Buch verschiedene Aspekte des Betriebssystems von Windows 8 bis Windows 10 Version 1703 ab. Des Weiteren heißen wir Pavel Yosifovich als neuen Co-Autor willkommen. Praktische Experimente Auch ohne Zugriff auf den Windows-Quellcode können Sie mithilfe des Kerneldebuggers, der Sysinternals-Tools und der eigens für dieses Buch entwickelten Werkzeuge viel über die internen Mechanismen von Windows herausfinden. Immer wenn ein Aspekt des internen Verhaltens von Windows mit einem der Tools sichtbar gemacht oder veranschaulicht werden kann, erfahren Sie in den mit »Experiment« betitelten Abschnitten, wie Sie dieses Tool selbst ausprobieren können. Diese Abschnitte sind über das ganze Buch verteilt, und wir raten Ihnen dazu, diese Experimente bei der Lektüre auszuführen. Es ist viel eindrucksvoller, die sichtbaren Auswirkungen der internen Mechanismen von Windows zu beobachten, als nur darüber zu lesen. Nicht behandelte Themen Windows ist ein umfangreiches und vielschichtiges Betriebssystem. In diesem Buch können wir nicht sämtliche internen Mechanismen von Windows abdecken, sondern konzentrieren uns auf xviii Einleitung
die grundlegenden Systemkomponenten. Beispielsweise werden COM+, die verteilte objekt­ orientierte Programmierinfrastruktur von Windows, und Microsoft .NET Framework, die Grundlage für Anwendungen mit verwaltetem Code, in diesem Buch nicht behandelt. Da es ein Buch über die »Interna« von Windows ist und kein Leitfaden für die Benutzung, die Programmierung oder die Systemadministration, erfahren Sie hier auch nicht, wie Sie Windows verwenden, programmieren oder konfigurieren. Warnung Dieses Buch beschreibt nicht dokumentierte Aspekte der internen Architektur und Funktionsweise von Windows (wie interne Kernelstrukturen und -funktionen), die sich zwischen den einzelnen Releases ändern können. Das soll nicht heißen, dass sie sich zwangsläufig ändern, sondern nur, dass Sie sich nicht auf ihre Unveränderbarkeit verlassen können. Software, die sich auf solche nicht dokumentierten Schnittstellen oder auf Insiderkenntnisse über das Betriebssystem verlässt, kann in zukünftigen Releases von Windows unter Umständen nicht mehr funktionieren. Software, die im Kernelmodus läuft (z. B. Gerätetreiber) und diese nicht dokumentierten Schnittstellen nutzt, kann bei der Ausführung in einer neueren Windows-Version sogar einen Systemabsturz und damit möglicherweise einen Datenverlust für den Benutzer hervorrufen. Kurz: Bei der Entwicklung von Software für Endbenutzersysteme und allgemein für jegliche Zwecke außer Forschung und Dokumentation sollten Sie niemals auf die in diesem Buch erwähnten internen Windows-Einrichtungen, Registrierungsschlüssel, Verhaltensweisen, APIs oder sonstigen nicht dokumentierten Elemente zurückgreifen. Suchen Sie immer erst auf MSDN (Microsoft Software Developer Network) nach einer offiziellen Dokumentation. Zielgruppe In diesem Buch wird vorausgesetzt, dass die Leser mit Windows auf dem Niveau von »Power-­ Usern« vertraut sind und Grundkenntnisse über Betriebssystem- und Hardwareelemente wie CPU-Register, Arbeitsspeicher, Prozesse und Threads mitbringen. In einigen Abschnitten sind auch Grundkenntnisse über Funktionen, Zeiger und ähnliche Konstrukte der Programmiersprache C von Nutzen. Der Aufbau dieses Buches Ebenso wie die sechste Ausgabe ist auch diese in zwei Bände aufgeteilt, von denen Sie den ersten in Händen halten. QQ Kapitel 1, »Begriffe und Werkzeuge«, gibt eine allgemeine Einführung in die Grundbegriffe von Windows und in die wichtigsten Werkzeuge, die wir in diesem Buch einsetzen werden. Es ist sehr wichtig, dieses Kapitel als erstes zu lesen, da es die Hintergrundinformationen für das Verständnis des Restes gibt. Einleitung xix
QQ Kapitel 2, »Systemarchitektur«, zeigt die Architektur und die Hauptkomponenten auf, aus denen Windows besteht, und erläutert sie. Mehrere dieser Komponenten werden in den folgenden Kapiteln noch ausführlicher beschrieben. QQ Kapitel 3, »Prozesse und Jobs«, erklärt, wie Prozesse in Windows implementiert sind, und zeigt die verschiedenen Vorgehensweisen zu ihrer Bearbeitung auf. Des Weiteren werden Jobs beschrieben, die eine Möglichkeit bieten, um eine Gruppe von Prozessen zu steuern und die Containerunterstützung von Windows zu aktivieren. QQ Kapitel 4, »Threads«, beschreibt ausführlich, wie Threads verwaltet, geplant und auf sonstige Weise in Windows bearbeitet werden. QQ Kapitel 5, »Speicherverwaltung«, zeigt, wie der Speicher-Manager physischen und virtuellen Arbeitsspeicher verwendet, und erklärt die verschiedenen Möglichkeiten, mit denen Prozesse und Treiber den Arbeitsspeicher bearbeiten und nutzen können. QQ Kapitel 6, »Das E/A-System«, beschreibt, wie das E/A-System von Windows funktioniert und wie es mit den Gerätetreibern zusammenwirkt, um die Mechanismen für E/A-Peripheriegeräte bereitzustellen. QQ Kapitel 7, »Sicherheit«, beschreibt ausführlich die verschiedenen Sicherheitsmechanismen von Windows einschließlich systemeigener Schutzmaßnahmen gegen Exploits. Schreibweisen In diesem Buch gelten die folgenden Schreibweisen: QQ Fettschrift wird für Text verwendet, den Sie in einer Schnittstelle eingeben müssen. QQ Kursivschrift kennzeichnet neue Begriffe sowie die Bezeichnungen von GUI-Elementen, Dateinamen und Webadressen. QQ Codeelemente stehen in nichtproportionaler Schrift. QQ Bei Tastaturkürzeln steht ein Pluszeichen zwischen den Bezeichnungen der einzelnen Tasten. Beispielsweise bedeutet (Strg) + (Alt) + (Entf), dass Sie gleichzeitig die Tasten (Strg), (Alt) und (Entf) drücken müssen. Begleitmaterial Damit Sie aus diesem Buch noch größeren Nutzen ziehen können, haben wir Begleitmaterial zusammengestellt, das Sie von der folgenden Seite herunterladen können: https://dpunkt.de/windowsinternals1 Den Quellcode der Tools, die wir eigens für dieses Buch geschrieben haben, finden Sie auf ­https://github.com/zodiacon/windowsinternals. xx Einleitung
Danksagungen Als Erstes möchten wir Pavel Yosifovich dafür danken, dass er sich uns für dieses Projekt angeschlossen hat. Seine Beteiligung war für die Veröffentlichung entscheidend. Die vielen Nächte, die er damit zugebracht hat, Einzelheiten von Windows zu studieren und über Änderungen während der Zeit von sechs Releases zu schreiben, haben dieses Buch erst möglich gemacht. Ohne die Rückmeldungen und die Unterstützung von wichtigen Mitgliedern des Windows-Entwicklungsteams und anderer Fachleute von Microsoft wäre dieses Buch auch technisch nicht so tief schürfend und nicht so präzise geworden, wie es ist. Daher möchten wir den folgenden Personen danken, die es auf technische Korrektheit durchgesehen, Informa­ tionen gegeben oder die Autoren auf andere Weise unterstützt haben: Akila Srinivasan, Alessandro Pilotti, Andrea Allievi, Andy Luhrs, Arun Kishan, Ben Hillis, Bill Messmer, Chris Kleynhans, Deepu Thomas, Eugene Bak, Jason Shirk, Jeremiah Cox, Joe Bialek, John Lambert, John Lento, Jon Berry, Kai Hsu, Ken Johnson, Landy Wang, Logan Gabriel, Luke Kim, Matt Miller, Matthew Woolman, Mehmet Iyigun, Michelle Bergeron, Minsang Kim, Mohamed Mansour, Nate Warfield, Neeraj Singh, Nick Judge, Pavel Lebedynskiy, Rich Turner, Saruhan Karademir, Simon Pope, Stephen Finnigan und Stephen Hufnagel. Wir möchten auch Ilfak Guilfanov von Hex-Rays (http://www.hex-rays.com) für die IDA Pro Advanced- und Hey-Rays-Lizenzen danken, die er Alex Ionescu vor mehr als einem Jahrzehnt zur Verfügung gestellt hat, um das Reverse-Engineering des Windows-Kernels zu beschleunigen, und für die fortlaufende Unterstützung und Entwicklung der Decompilerfeatures, die es überhaupt erst möglich gemacht haben, ein solches Buch ohne Zugriff auf den Quellcode zu schreiben. Zu guter Letzt möchten die Autoren den großartigen Mitarbeitern bei Microsoft Press denken, die dieses Buch verwirklicht haben. Devon Musgrave hat zum letzten Mal als unser Akquiseleiter gearbeitet, während Kate Shoup den Titel als Projektleiterin beaufsichtigt hat. Auch Shawn Morningstar, Kelly Talbot und Corina Lebegioara haben zu der Qualität dieses Buches beigetragen. Errata Wir haben uns gewissenhaft darum bemüht, dieses Buch und das Begleitmaterial so korrekt wie möglich zu gestalten. Jegliche Fehler in der englischen Originalausgabe, die seit der Veröffentlichung des Buches gemeldet wurden, sind auf der Website von Microsoft Press unter folgender Adresse aufgeführt: https://aka.ms/winint7ed/errata Mit Anmerkungen, Fragen oder Verbesserungsvorschlägen zu diesem Buch können Sie sich aber auch an den deutschen dpunkt.verlag wenden: hallo@dpunkt.de Bitte beachten Sie, dass über unsere E-Mail-Adressen kein Support für Software und Hardware angeboten wird. Für Supportinformationen bezüglich der Soft- und Hardwareprodukte besuchen Sie die Microsoft-Website http://support.microsoft.com. Einleitung xxi
Kontakt Bei Microsoft Press steht Ihre Zufriedenheit an oberster Stelle. Daher ist Ihr Feedback für uns sehr wichtig. Lassen Sie uns auf dieser englischsprachigen Website wissen, wie Sie dieses Buch finden: https://aka.ms/tellpress Wir wissen, dass Sie viel zu tun haben. Darum finden Sie auf der Webseite nur wenige Fragen. Ihre Antworten gehen direkt an das Team von Microsoft Press. (Es werden keine persönlichen Informationen abgefragt.) Im Voraus vielen Dank für Ihre Unterstützung. Über Ihr Feedback per E-Mail freut sich außerdem der dpunkt.verlag über: hallo@dpunkt.de Falls Sie News, Updates usw. zu Microsoft Press-Büchern erhalten möchten, können Sie uns auf Twitter folgen: http://twitter.com/MicrosoftPress (Englisch) https://twitter.com/dpunkt_verlag (Deutsch) xxii Einleitung
K apite l 1 Begriffe und Werkzeuge In diesem Kapitel führen wir Kernbegriffe des Betriebssystems Microsoft Windows ein, die wir in dem gesamten Buch verwenden werden, z. B. Windows-API, Prozesse, Threads, virtueller Speicher, Kernel- und Benutzermodus, Objekte, Handles, Sicherheit und Registrierung. Außerdem stellen wir die Werkzeuge vor, mit denen Sie die inneren Mechanismen von Windows erkunden können, darunter den Kernel-Debugger, die Leistungsüberwachung und die wichtigsten Werkzeuge aus Windows Sysinternals (http://www.microsoft.com/technet/sysinternals). Des Weiteren erklären wir, wie Sie das Windows Driver Kit (WDK) und das Windows Software Developer Kit (SDK) verwenden können, um mehr über Windows-Interna zu erfahren. In dem Rest dieses Buches wird vorausgesetzt, dass Sie die in diesem Kapitel eingeführten Begriffe kennen. Versionen des Betriebssystems Windows In diesem Buch geht es um die neueste Version der Client- und Serverbetriebssysteme von Microsoft, nämlich Windows 10 (eine 32-Bit-Version für x86 und ARM sowie eine 64-Bit-Version für x64) und Windows Server 2016 (wovon es nur eine 64-Bit-Version gibt). Sofern nicht ausdrücklich anders angegeben, bezieht sich der Text auf alle diese Versionen. Als Hintergrundinformation finden Sie in Tabelle 1–1 die Produktnamen, internen Versionsnummern und Veröffentlichungsdaten der bisherigen Windows-Versionen. Produktname Interne Versionsnummer Veröffentlichungsdatum Windows NT 3.1 3.1 Juli 1993 Windows NT 3.5 3.5 September 1994 Windows NT 3.51 3.51 Mai 1995 Windows NT 4.0 4.0 Juli 1996 Windows 2000 5.0 Dezember 1999 Windows XP 5.1 August 2001 Windows Server 2003 5.2 März 2003 Windows Server 2003 R2 5.2 Dezember 2005 Windows Vista 6.0 Januar 2007 Windows Server 2008 6.0 (Service Pack 1) März 2008 Windows 7 6.1 Oktober 2009 Windows Server 2008 R2 6.1 Oktober 2009 → 1
Produktname Interne Versionsnummer Veröffentlichungsdatum Windows 8 6.2 Oktober 2012 Windows Server 2012 6.2 Oktober 2012 Windows 8.1 6.3 Oktober 2013 Windows Server 2012 R2 6.3 Oktober 2013 Windows 10 10.0 (Build 10240) Juli 2015 Windows 10 Version 1511 10.0 (Build 10586) November 2015 Windows 10 Version 1607 (Anniversary Update) 10.0 (Build 14393) Juli 2016 Windows Server 2016 10.0 (Build 14393) Oktober 2016 Tabelle 1–1 Windows-Betriebssysteme Beginnend mit Windows 7 scheint bei der Vergabe der Versionsnummern von einem wohldefinierten Muster abgewichen worden zu sein. Die Versionsnummer dieses Betriebssystems lautete nicht 7, sondern 6.1. Als mit Windows Vista die Versionsnummer auf 6.0 erhöht wurde, konnten manche Anwendungen das richtige Betriebssystem nicht erkennen. Das lag daran, dass die Entwickler aufgrund der Beliebtheit von Windows XP nur auf eine Überprüfung auf Hauptversionsnummern größer oder gleich 5 und Nebenversionsnummern größer oder gleich 1 durchführten. Diese Bedingung aber war bei Windows Vista nicht gegeben. Microsoft hatte die Lektion daraus gelernt und im Folgenden die Hauptversion bei 6 belassen und die Nebenversionsnummer auf mindestens 2 (größer 1) festgelegt, um solche Inkompatibilitäten zu minimieren. Mit Windows 10 wurde die Versionsnummer jedoch auf 10.0 heraufgesetzt. Beginnend mit Windows 8 gibt die Funktion GetVersionEx der Windows-API als Betriebssystemnummer unabhängig vom tatsächlich vorhandenen Betriebssystem standardmäßig 6.2 (Windows 8) zurück. (Diese Funktion ist außerdem als unerwünscht eingestuft.) Das dient zur Minimierung von Kompatibilitätsproblemen, zeigt aber auch, dass die Überprüfung der Betriebssystemversion in den meisten Fällen nicht die beste Vorgehensweise ist, weil manche Komponenten unabhängig von einem bestimmten offiziellen Windows-Release installiert werden können. Wenn Sie dennoch die tatsächliche Betriebssystemversion benötigen, können Sie sie indirekt bestimmen, indem Sie die Funktion VerifyVersionInfo oder die neuen Hilfs-APIs für die Versionsbestimmung wie IsWindows80rGreater, IsWindows8Point​ 10rGreater, IsWindows100rGreater, IsWindowsServer u. Ä. verwenden. Außerdem lässt sich die Betriebssystemkompatibilität im Manifest der ausführbaren Datei angeben, was die Ergebnisse dieser Funktion ändert. (Einzelheiten entnehmen Sie bitte Kapitel 8, »Systemmechanismen«, in Band 2.) Informationen über die Windows-Version können Sie mit dem Befehlszeilenwerkzeug ver oder grafisch durch Ausführen von winver einsehen. Der folgende Screenshot zeigt das Ergebnis von winver in Windows 10 Enterprise Version 1511: 2 Kapitel 1 Begriffe und Werkzeuge
Das Dialogfeld zeigt auch die Buildnummer (hier 10586.218), die für Windows-Insider hilfreich sein mag (also für diejenigen, die sich registriert haben, um Vorabversionen von Windows zu erhalten). Sie ist auch praktisch für die Verwaltung von Sicherheitsaktualisierungen, da sie zeigt, welche Patches installiert sind. Windows 10 und zukünftige Windows-Versionen Mit dem Erscheinen von Windows 10 hat Microsoft angekündigt, Windows jetzt in schnellerer Folge zu aktualisieren als zuvor. Es wird kein offizielles Windows 11 geben. Stattdessen wird Windows Update (oder ein anderes Bereitstellungsmodell) das vorhandene Windows 10 auf eine neue Version aktualisieren. Zurzeit hat es schon zwei solcher Aktualisierungen gegeben, nämlich im November 2015 (Version 1511, wobei sich die Nummer auf Jahr und Monat der Bereitstellung bezieht) und im Juli 2016 (Version 1607, die auch unter der Bezeichnung Anniversary Update vermarktet wurde). Intern wird Windows nach wie vor phasenweise weiterentwickelt. Beispielsweise trug das ursprüngliche Release von Windows 10 den Codenamen Threshold 1 und die Aktualisierung von November 2015 die Bezeichnung Threshold 2. Die nächsten drei Phasen sind Redstone 1 (Version 1607), auf die Redstone 2 und Redstone 3 folgen werden. Windows 10 und OneCore Mit den Jahren wurden verschiedene Geschmacksrichtungen von Windows entwickelt. Neben dem Standard-Windows für PCs gibt es auch das Betriebssystem für die Spielkonsole Xbox 360, bei dem es sich um eine Fork von Windows 2000 handelt. Die Variante Windows Phone 7 basierte auf Windows CE (dem Echtzeit-Betriebssystem von Microsoft). Die Pflege und Erweiterung all dieses Codes ist naturgemäß nicht einfach. Daher hat man bei Microsoft entschieden, die Kernels und die Binärdateien für die grundlegende Plattform in einer einzigen zusammenfassen. Das begann bei Windows 8 und Windows Phone 8, die bereits über einen gemeinsamen Kernel verfügten (wobei Window 8.1. und Windows Phone 8.1 auch eine gemeinsame Versionen des Betriebssystems Windows Kapitel 1 3
Windows-Laufzeit-API hatten). Mit Windows 10 ist diese Zusammenlegung abgeschlossen. Die gemeinsame Plattform heißt OneCore und läuft auf PCs, Smartphones, der Spielkonsole Xbox One, der HoloLens und auf IoT-Geräten (für das »Internet der Dinge«) wie dem Raspberry Pi 2. Diese Geräte unterscheiden sich in ihrer physischen Gestalt sehr stark voneinander, weshalb einige Merkmale nicht überall zur Verfügung stehen. So ist es beispielsweise nicht sinnvoll, für HoloLens-Geräte eine Unterstützung für Maus und Tastatur anzubieten, weshalb diese Teile in der Windows 10-Version dafür nicht vorhanden sind. Aber der Kernel, die Treiber und die Binärdateien der grundlegenden Plattform sind im Prinzip identisch (mit registrierungs- oder richtliniengestützten Einstellungen, wo dies für die Leistung oder aus anderen Gründen sinnvoll ist). Ein Beispiel für solche Richtlinien finden Sie im Abschnitt »API-Sets« in Kapitel 3, »Prozesse und Jobs«. In diesem Buch geht es um die inneren Mechanismen des OneCore-Kernels unabhängig von dem Gerät, auf dem er läuft. Die Experimente in diesem Buch sind jedoch für Desktop-Computer mit Maus und Tastatur ausgelegt, allerdings hauptsächlich aus praktischen Gründen, da es nicht einfach (und manchmal sogar unmöglich) ist, die gleichen Versuche auf Geräten wie Smartphones oder Xbox One durchzuführen. Grundprinzipien und -begriffe In den folgenden Abschnitten geben wir eine Einführung in die Grundprinzipien und -begriffe von Windows, deren Kenntnis für das Verständnis dieses Buches unverzichtbar ist. Viele der hier genannten Elemente wie Prozesse, Threads und virtueller Speicher werden in den nachfolgenden Kapiteln noch ausführlicher dargestellt. Die Windows-API Die Windows-API ist die Systemprogrammierschnittstelle der Windows-Betriebssystemfamilie für den Benutzermodus. Vor der Einführung der 64-Bit-Versionen von Windows wurde die 32-Bit-APi als Win32API bezeichnet, um sie von der ursprünglichen 16-Bit-API zu unterscheiden. Die Bezeichnung Windows-API in diesem Buch bezieht sich sowohl auf die 32- als auch die 64-Bit-Programmierschnittstelle von Windows. Manchmal verwenden wir auch den Begriff Win32API statt Windows-API. Aber auch dann ist sowohl die 32- als auch die 64-Bit-Variante gemeint. Die Windows-API wird in der Dokumentation des Windows-SDK beschrieben (siehe den Abschnitt »Windows Software Development Kit« weiter hinten in diesem Kapitel), die kostenlos auf https://developer.microsoft.com/en-us/windows/ desktop/develop zur Verfügung steht und in allen Abonnements von MSDN, dem Microsoft-Unterstützungsprogramm für Entwickler, eingeschlossen ist. Eine hervorragende Beschreibung der Programmierung für die grundlegende WindowsAPI finden Sie in dem Buch Windows via C/C++, Fifth Edition von Jeffrey Richter und Christophe Nasarre (Microsoft Press, 2007). 4 Kapitel 1 Begriffe und Werkzeuge
Varianten der Windows-API Die Windows-API bestand ursprünglich nur aus C-artigen Funktionen. Heutzutage können Entwickler auf Tausende solcher Funktionen zurückgreifen. Bei der Erfindung von Windows bot sich die Sprache C an, da sie den kleinsten gemeinsamen Nenner darstellte (weil sie auch von anderen Sprachen aus zugänglich war) und maschinennah genug, um Betriebssystemdienste bereitzustellen. Der Nachteil bestand in einer ungeheuren Menge von Funktionen ohne konsistente Benennung und logische Gruppierung (etwa durch einen Mechanismus wie die Namespaces von C++). Aufgrund dieser Schwierigkeiten wurden schließlich neuere APIs verwendet, die einen anderen API-Mechanismus einsetzten, nämlich das Component Object Model (COM). COM wurde ursprünglich entwickelt, um Microsoft Office-Anwendungen die Kommunikation und den Datenaustausch zwischen Dokumenten zu ermöglichen (z. B. die Einbettung eines Excel-Diagramms in ein Word-Dokument oder eine PowerPoint-Präsentation). Diese Fähigkeit wird als OLE (Object Linking and Embedding) bezeichnet und wurde ursprünglich mit dem alten Windows-Messagingmechanismus DDE (Dynamic Data Exchange) implementiert. Aufgrund der Einschränkungen, die DDE zu eigen waren, wurde allerdings eine neue Kommunikationsmöglichkeit entwickelt, und das war COM. Als COM um 1993 veröffentlicht wurde, hieß es sogar ursprünglich OLE 2. COM basiert auf zwei Grundprinzipien: Erstens kommunizieren Clients mit Objekten (manchmal COM-Serverobjekte genannt) über Schnittstellen. Dies sind wohldefinierte Kontrakte mit einem Satz logisch verwandter Methoden, die durch einen Zuteilungsmechanismus für virtuelle Tabellen gruppiert werden. Dies ist eine übliche Vorgehensweise von C++-Compilern zur Implementierung einer Zuteilung für virtuelle Funktionen. Sie resultiert in Binärkompatibilität und verhindert Probleme durch Namensverstümmelung im Compiler. Dadurch wird es möglich, die Methoden von vielen Sprachen (und Compilern) aufzurufen, z. B. C, C++, Visual Basic, .NET-Sprachen, Delphi usw. Das zweite Prinzip lautet, dass die Implementierung von Komponenten dynamisch geladen und nicht statisch mit dem Client verlinkt wird. Der Begriff COM-Server bezieht sich gewöhnlich auf eine DLL (Dynamic Link Library) oder eine ausführbare Datei (EXE), in der die COM-Klassen implementiert sind. COM weist noch weitere wichtige Merkmale zu Sicherheit, prozessübergreifendem Marshalling, Threading usw. auf. Eine umfassende Darstellung von COM können wir in diesem Buch nicht leisten. Eine hervorragende Abhandlung über COM finden Sie in dem Buch Essential COM von Don Box (Addison-Wesley, 1998). Über COM zugängliche APIs sind beispielsweise DirectShow, Windows Media Foundation, DirectX, DirectComposition, WIC (Windows Imaging Component) und BITS (Background Intelligent Transfer Service). Die Windows-Laufzeitumgebung Mit Windows 8 wurden eine neue API und eine neue Laufzeitumgebung eingeführt, die Windows Runtime (manchmal auch als WinRT abgekürzt; nicht zu verwechseln mit dem inzwischen eingestellten Betriebssystem Windows RT für ARM-Prozessoren). Die Windows-Laufzeitumgebung Grundprinzipien und -begriffe Kapitel 1 5
besteht aus Plattformdiensten für die Entwickler von sogenannten Windows-Apps (die früher auch als Metro Apps, Modern Apps, Immersive Apps und Windows Store Apps bezeichnet wurden). Windows-Apps können auf unterschiedlichen physischen Geräten ausgeführt werden, von kleinen IoT-Geräten über Telefone und Tablets bis zu Laptop- und Desktop-Computern und sogar auf Geräten wie die Xbox One und die Microsoft HoloLens. Aus der Sicht der API setzt WinRT auf COM auf und erweitert die grundlegende COM-In­ frastruktur. Beispielsweise sind in WinRT vollständige Typmetadaten verfügbar (die im .NET-Metadatenformat in WINMDF-Dateien gespeichert werden), was die COM-Typbibliotheken erweitert. Mit Namespace-Hierarchien, konsistenter Benennung und Programmiermustern ist WinRT auch viel zusammenhängender gestaltet als klassische Windows-API-Funktionen. Anders als normale Windows-Anwendungen (die jetzt als Windows-Desktop-Anwendungen oder klassische Windows-Anwendungen bezeichnet werden) unterliegen Windows-Apps neuen Regeln. Diese Regeln werden in Kapitel 9, »Verwaltungsmechanismen«, von Band 2 beschrieben. Die Beziehungen zwischen den verschiedenen APIs und Anwendungen sind nicht offensichtlich. Desktop-Anwendungen können einen Teil der WinRT-APIs verwenden, WindowsApps dagegen eine der Win32- und COM-APIs. Um jeweils genau zu erfahren, welche APIs für welche Anwendungsplattformen zur Verfügung stehen, schlagen Sie bitte in der MSDN-Dokumentation nach. Beachten Sie jedoch, dass die WinRT-API auf der grundlegenden binären Ebene immer noch auf den älteren Binärdateien und APIs von Windows basiert, auch wenn bestimmte APIs nicht unterstützt werden und ihre Verfügbarkeit nicht dokumentiert ist. Es gibt keine neue »native« API für das System. Das ist vergleichbar mit .NET, das nach wie vor auf die traditionelle Windows-API zurückgreift. Anwendungen, die in C++, C#, anderen .NET-Sprachen oder JavaScript geschrieben sind, können WinRT-APIs dank der für diese Plattformen entwickelten Sprachprojektionen leicht nutzen. Für C++ hat Microsoft eine nicht standardmäßige Erweiterung namens C++/CX erstellt, die die Verwendung von WinRT-Typen vereinfacht. Die normale COM-Interop-Schicht für .NET (zusammen mit einigen unterstützenden Laufzeiterweiterungen) erlaubt allen .NET-Sprachen die natürliche und einfache Nutzung von WinRT-APIs wie in reinem .NET. Für JavaScript-Entwickler gibt es die Erweiterung WinJS für den Zugriff auf WinRT. Für die Gestaltung der Benutzeroberfläche müssen JavaScript-Entwickler allerdings nach wie vor HTML einsetzen. HTML kann zwar in Windows-Apps eingesetzt werden, allerdings handelt es sich dabei immer noch um eine lokale Client-App und nicht um eine von einem Webserver abgerufene Webanwendung. .NET Framework .NET Framework gehört zu Windows. Tabelle 1–2 gibt an, welche Version davon jeweils in den einzelnen Windows-Versionen installiert ist. Es ist jedoch immer möglich, auch eine neuere Version zu installieren. 6 Kapitel 1 Begriffe und Werkzeuge
Windows-Version .NET Framework-Version Windows 8 4.5 Windows 8.1 4.5.1 Windows 10 4.6 Windows 10 Version 1511 4.6.1 Windows 10 Version 1607 4.6.2 Tabelle 1–2 .NET Framework-Standardinstallation in Windows .NET Framework besteht aus den beiden folgenden Hauptkomponenten: QQ CLR (Common Language Runtime) Dies ist die Laufzeit-Engine für .NET. Sie umfasst einen JIT-Compiler (Just In Time), der CIL-Anweisungen (Common Intermediate Language) in die Maschinensprache der CPU übersetzt, einen Garbage Collector, Typverifizierung, Codezugriffssicherheit usw. Sie ist als In-Process-COM-Server (DLL) implementiert und nutzt verschiedene Einrichtungen, die von der Windows-API bereitgestellt werden. QQ .NET Framework-Klassenbibliothek (FCL) Dies ist eine umfangreiche Sammlung von Typen für die Funktionalität, die Client- und Serveranwendungen gewöhnlich benötigen, z. B. Benutzerschnittstellendienste, Netzwerkzugriff, Datenbankzugriff usw. Durch diese und weitere Merkmale, darunter neue High-Level-Programmiersprachen (C#, Visual Basic, F#) und Unterstützungswerkzeuge, steigert .NET Framework die Produktivität von Entwicklern und erhöht Sicherheit und Zuverlässigkeit der Anwendungen. Abb. 1–1 zeigt die Beziehung zwischen .NET Framework und dem Betriebssystem. Benutzermodus (verwalteter Code) .NET-App (EXE) FCL (.NET-Assemblies) (DLLs) Benutzermodus (nativer Code) CLR (COM-DLL-Server) Windows-API-DLLs Kernelmodus Abbildung 1–1 Windows-Kernel Die Beziehung zwischen .NET und Windows Dienste, Funktionen und Routinen Mehrere Begriffe in der Windows-Dokumentation für Benutzer und für Programmierer haben je nach Kontext eine unterschiedliche Bedeutung. Beispielsweise kann es sich bei einem Dienst um eine aufrufbare Routine im Betriebssystem, um einen Gerätetreiber oder um einen Server- Grundprinzipien und -begriffe Kapitel 1 7
prozess handeln. Die folgende Aufstellung gibt an, was in diesem Buch mit den genannten Begriffen gemeint ist: QQ Windows-API-Funktionen Dies sind dokumentierte, aufrufbare Subroutinen in der Windows-API, beispielsweise CreateProcess, CreateFile und GetMessage. QQ Native Systemdienste (Systemaufrufe) Dies sind nicht dokumentierte, zugrunde liegende Dienste im Betriebssystem, die vom Benutzermodus aus aufgerufen werden können. Beispielsweise ist NtCreateUserProcess der interne Systemdienst, den die Windows-Funk­ tion CreateProcess aufruft, um einen neuen Prozess zu erstellen. QQ Kernelunterstützungsfunktionen (Routinen) Dies sind Subroutinen in Windows, die ausschließlich im Kernelmodus aufgerufen werden können (der weiter hinten in diesem Kapitel erklärt wird). Beispielsweise ist ExAllocatePoolWithTag die Routine, die Gerätetreiber aufrufen, um Speicher von den Windows-Systemheaps (den sogenannten Pools) zuzuweisen. QQ Windows-Dienste Dies sind Prozesse, die vom Dienststeuerungs-Manager von Windows gestartet werden. Beispielsweise wird der Taskplaner-Dienst in einem Benutzermodusprozess ausgeführt, der den Befehl schtasks unterstützt (der den UNIX-Befehlen at und cron ähnelt). (In der Registrierung werden Windows-Gerätetreiber zwar als »Dienste« bezeichnet, nicht aber in diesem Buch.) QQ DLLs (Dynamic Link Libraries) Eine DLL ist eine Binärdatei, zu der aufrufbare Subroutinen verknüpft worden sind und die von Anwendungen, die diese Subroutinen nutzen, dynamisch geladen werden kann, beispielsweise Msvcrt.dll (die C-Laufzeitbibliothek) und Kernel32.dll (eine der Teilsystem-Bibliotheken der Windows-API). Komponenten des Windows-Benutzermodus und Anwendungen nutzen DLLs ausgiebig. Der Vorteil von DLLs gegenüber statischen Bibliotheken besteht darin, dass Anwendungen sie gemeinsam nutzen, wobei Windows dafür sorgt, dass es im Arbeitsspeicher nur ein einziges Exemplar des DLL-Codes gibt, auf die alle Anwendungen zugreifen. Bibliotheks-Assemblies von .NET werden als DLLs kompiliert, aber ohne nicht verwaltete, exportierte Subroutinen. Stattdessen parst die CLR die kompilierten Metadaten, um auf die entsprechenden Typen und Elemente zuzugreifen. Prozesse Programme und Prozesse scheinen zwar oberflächlich gesehen ähnlich zu sein, unterscheiden sich aber grundlegend voneinander. Ein Programm ist eine statische Folge von Anweisungen, wohingegen es sich bei einem Prozess um einen Container für einen Satz von Ressourcen handelt, die bei der Ausführung einer Instanz des Programms genutzt werden. Grob gesehen, besteht ein Windows-Prozess aus folgenden Elementen: QQ Privater virtueller Adressraum Dies ist ein Satz von virtuellen Speicheradressen, die der Prozess nutzen kann. QQ Ausführbares Programm Das Programm definiert den ursprünglichen Code und die Daten und wird auf den virtuellen Adressraum des Prozesses abgebildet. 8 Kapitel 1 Begriffe und Werkzeuge
QQ Ein Satz offener Handles Diese Handles werden verschiedenen Systemressourcen zugeordnet, z. B. Semaphoren, Synchronisierungsobjekten und Dateien, und sind von allen Threads im Prozess aus zugänglich. QQ Sicherheitskontext Hierbei handelt es sich um ein Zugriffstoken, das den Benutzer, die Sicherheitsgruppen, die Rechte, Attribute, Claims und Fähigkeiten, den Virtualisierungsstatus der Benutzerkontensteuerung, die Sitzung und den Zustand des eingeschränkten Benutzerzugriffs angibt, die mit dem Prozess verbunden sind. Des Weiteren legt der Kontext den Bezeichner des Anwendungscontainers fest und enthält Angaben über die zugehörige Sandbox. QQ Prozesskennung Dieser eindeutige Bezeichner ist Teil eines weiteren Bezeichners, nämlich der Clientkennung. QQ Mindestens ein Ausführungsthread tens) nicht sinnvoll. Ein »leerer« Prozess ist zwar möglich, aber (meis- Es gibt eine Reihe von Werkzeugen, um Prozesse und Prozessinformationen einzusehen und zu bearbeiten. Die folgenden Experimente zeigen die verschiedenen Ansichten von Prozessinformationen, die Sie mit einigen dieser Tools gewinnen können. Manche dieser Werkzeuge sind in Windows selbst, im Debugging Tool for Windows oder im Windows SDK enthalten, während es sich bei anderen um eigenständige Programme von Sysinternals handelt. Viele dieser Werkzeuge zeigen die gleichen Kerninformationen über Prozesse und Threads an, manchmal jedoch unter unterschiedlichen Bezeichnungen. Das wahrscheinlich am häufigsten verwendete Werkzeug zur Untersuchung von Prozessaktivitäten ist der Task-Manager. (Der Name ist allerdings etwas eigenartig, da es im Windows-Kernel keine »Tasks« gibt.) Das folgende Experiment führt einige der Grundfunktionen dieses Programms vor. Experiment: Prozessinformationen im Task-Manager einsehen Der im Lieferumfang von Windows enthaltene Task-Manager gibt eine Übersicht über die Prozesse im System. Sie können ihn auf vier verschiedene Weisen starten: QQ Drücken Sie (Strg) + (Umschalt) + (Esc). QQ Rechtsklicken Sie auf die Taskleiste und wählen Sie Task-Manager starten. QQ Drücken Sie (Strg) + (Alt) + (Entf) und klicken Sie auf die Schaltfläche Task-Manager starten. QQ Starten Sie die ausführbare Datei Taskmgr.exe. → Grundprinzipien und -begriffe Kapitel 1 9
Wenn Sie den Task-Manager zum ersten Mal starten, befindet er sich im Übersichtsmodus, in dem nur die Prozesse angezeigt werden, für die es ein sichtbares Fenster gibt: In diesem Fenster können Sie allerdings nicht viel sehen. Klicken Sie daher auf die Erweiterungsschaltfläche Details, um in die Vollansicht zu wechseln. Dabei liegt standardmäßig die Registerkarte Prozesse oben: Die Registerkarte Prozesse zeigt eine Liste der Prozesse mit vier Spalten: CPU, Arbeitsspeicher, Datenträger und Netzwerk. Wenn Sie auf die Kopfzeile rechtsklicken, können Sie noch weitere Spalten einblenden lassen. Zur Verfügung stehen Prozessname (Abbild), Prozess-ID, Typ, Status, Herausgeber und Befehlszeile. Einige Prozesse können erweitert werden, sodass Sie erkennen können, welche sichtbaren Fenster von ihnen erstellt werden. Um noch mehr Einzelheiten zu den Prozessen einzusehen, klicken Sie auf die Registerkarte Details. Sie können auch auf einen Prozess rechtsklicken und Zu Details wechseln auswählen. Dadurch gelangen Sie auf die Registerkarte Details, wobei der betreffende Prozess schon ausgewählt ist. → 10 Kapitel 1 Begriffe und Werkzeuge
Die Registerkarte Prozesse im Task-Manager von Windows 7 entspricht ungefähr der Registerkarte Details im Task-Manager von Windows 8 und höher. Im Task-Manager von Windows 7 zeigt die Registerkarte Anwendungen nur Prozesse mit sichtbaren Fenstern, und nicht sämtliche Prozesse. Diese Angaben sind jetzt auf der Registerkarte Prozesse des neuen Task-Managers von Windows 8 und höher enthalten. Die Registerkarte Details zeigt ebenfalls Prozesse, aber auf kompaktere Art und Weise. Sie gibt die von den Prozessen erstellten Fenster nicht an, enthält aber eine breitere Auswahl von Informationsspalten. Prozesse werden anhand ihres Namens bezeichnet und mit einem Symbol dargestellt, das angibt, wovon sie eine Instanz darstellen. Anders als viele andere Objekte in Windows können Prozesse keine globalen Namen bekommen. Um weitere Einzelheiten einzublenden, rechtsklicken Sie auf die Kopfzeile und wählen Spalten auswählen. Dadurch wird eine Liste von Spalten angezeigt: → Grundprinzipien und -begriffe Kapitel 1 11
Unter anderem gibt es folgende wichtige Spalten: QQ Threads Die Spalte Threads zeigt die Anzahl der Threads in jedem Prozess. Normalerweise sollte sie mindestens eins betragen, da es keine unmittelbare Möglichkeit gibt, um einen Prozess ohne Thread zu erstellen (und ein solcher Prozess auch nicht sehr sinnvoll wäre). Werden für einen Prozess null Threads angezeigt, so bedeutet das gewöhnlich, dass der Prozess aus irgendeinem Grund nicht gelöscht werden kann, wahrscheinlich, weil der Treibercode fehlerhaft ist. QQ Handles Die Spalte Handles zeigt die Anzahl der Handles von Kernelobjekten, die die Threads in dem Prozess geöffnet haben. (Dieser Vorgang wird weiter hinten in diesem Kapitel sowie ausführlich in Kapitel 8 von Band 2 beschrieben.) QQ Status Die Spalte Status ist etwas knifflig. Bei Prozessen ohne Benutzerschnittstelle sollte hier normalerweise Wird ausgeführt angezeigt werden, wobei es jedoch sein kann, dass alle Threads auf irgendetwas warten, etwa auf ein Signal an ein Kernelobjekt oder den Abschluss einer E/A-Operation. Die andere Möglichkeit für einen solchen Prozess ist Angehalten, was angezeigt wird, wenn alle Threads in dem Prozess angehalten sind. Es ist sehr unwahrscheinlich, dass dies durch den Prozess selbst verursacht wird, lässt sich aber programmgesteuert durch den Aufruf der nicht dokumentierten nativen API NtSuspendProcess für den Prozess erreichen, gewöhnlich mithilfe eines Tools (beispielsweise gibt es in dem weiter hinten beschriebenen Process Explorer eine solche Option). Für Prozesse mit Benutzerschnittstelle bedeutet der Statuswert Wird ausgeführt, dass die Benutzerschnittstelle reagiert. Mit anderen Worten, der Thread, der die Fenster erstellt hat, wartet auf Benutzereingaben (technisch gesehen auf die mit ihm verbundene Nachrichtenwarteschlange). Der Zustand Angehalten ist ebenso möglich wie bei Prozessen ohne Benutzerschnittstelle, allerdings tritt er bei Windows-Apps (also denen mit Windows Runtime) gewöhnlich auf, wenn die Anwendung vom Benutzer minimiert wird und dadurch ihren Vordergrundstatus einbüßt. Solche Prozesse werden nach fünf Sekunden angehalten, damit sie keine CPU- und Netzwerkressourcen verbrauchen. Dadurch kann die neue Vordergrundanwendung alle Computerressourcen erhalten. Das ist insbesondere auf akkubetriebenen Geräten wie Tablets und Smartphones wichtig. Dieser und ähnliche Mechanismen werden ausführlich in Kapitel 9 von Band 2 beschrieben. Der dritte mögliche Wert für Status lautet Reagiert nicht. Das kann geschehen, wenn ein Thread in dem Prozess für die Benutzerschnittstelle seine Nachrichtenwarteschlange für Aktivitäten im Zusammenhang mit der Schnittstelle mindestens fünf Sekunden lang nicht überprüft hat. Der Prozesse (eigentlich der Thread, dem das Fenster gehört) kann mit irgendwelchen CPU-intensiven Arbeiten beschäftigt sein oder auf etwas völlig anderes warten (z. B. auf den Abschluss einer E/A-Operation). In jedem Fall friert die Benutzeroberfläche ein, was Windows dadurch kennzeichnet, dass die fraglichen Fenster blasser dargestellt und ihren Titeln der Zusatz (reagiert nicht) hinzugefügt wird. 12 Kapitel 1 Begriffe und Werkzeuge
Jeder Prozess verweist außerdem auf seinen Elternprozess (bei dem es sich um seinen Erstellerprozess handeln kann, aber nicht muss). Existiert der Elternprozess nicht mehr, so wird diese Angabe auch nicht mehr aktualisiert. Daher kann es sein, dass ein Prozess auf einen Elternprozess verweist, den es nicht mehr gibt. Das ist jedoch kein Problem, da es nichts gibt, was eine aktuelle Angabe darüber benötigt. Dieses Verhalten wird durch das folgende Experiment veranschaulicht. (Im Tool Process Explorer wird der Startzeitpunkt des Elternprozesses berücksichtigt, um zu verhindern, dass ein Kindprozess auf der Grundlage einer wiederverwendeten Prozesskennung angefügt wird.) Wann ist der Elternprozess nicht mit dem Erstellerprozess identisch? Bei einigen Prozessen, die von bestimmten Benutzeranwendungen erstellt werden, kann die Hilfe eines Makler- oder Hilfsprozesses erforderlich sein, der die API zur Prozesserstellung aufruft. In einem solchen Fall wäre es verwirrend (oder sogar falsch, wenn eine Vererbung von Handles oder Adressräumen erforderlich ist), den Maklerprozess als Ersteller anzuzeigen, weshalb eine Umdefinition des Elternprozesses erfolgt. Mehr darüber erfahren Sie in Kapitel 7, »Sicherheit«. Experiment: Die Prozessstruktur einsehen Die meisten Werkzeuge zeigen für einen Prozess nicht die ID des Eltern- oder des Erstellerprozesses an. Diesen Wert können Sie jedoch in der Leistungsüberwachung (oder programmgesteuert) gewinnen, indem Sie Prozesskennung erstellen abrufen (was eigentlich »Kennung des erstellenden Prozesses« heißen müsste). In Tlist.exe aus den Debugging Tools for Windows können Sie die Prozessstruktur mithilfe des Optionsschalters /t anzeigen lassen. Im Folgenden sehen Sie eine Beispielausgabe von tlist /t: System Process (0) System (4) smss.exe (360) csrss.exe (460) wininit.exe (524) services.exe (648) svchost.exe (736) unsecapp.exe (2516) WmiPrvSE.exe (2860) WmiPrvSE.exe (2512) RuntimeBroker.exe (3104) SkypeHost.exe (2776) ShellExperienceHost.exe (3760) Windows Shell Experience Host ApplicationFrameHost.exe (2848) OleMainThreadWndName SearchUI.exe (3504) Cortana WmiPrvSE.exe (1576) → Grundprinzipien und -begriffe Kapitel 1 13
TiWorker.exe (6032) wuapihost.exe (5088) svchost.exe (788) svchost.exe (932) svchost.exe (960) svchost.exe (976) svchost.exe (68) svchost.exe (380) VSSVC.exe (1124) svchost.exe (1176) sihost.exe (3664) taskhostw.exe (3032) Task Host Window svchost.exe (1212) svchost.exe (1636) spoolsv.exe (1644) svchost.exe (1936) OfficeClickToRun.exe (1324) MSOIDSVC.EXE (1256) MSOIDSVCM.EXE (2264) MBAMAgent.exe (2072) MsMpEng.exe (2116) SearchIndexer.exe (1000) SearchProtocolHost.exe (824) svchost.exe (3328) svchost.exe (3428) svchost.exe (4400) svchost.exe (4360) svchost.exe (3720) TrustedInstaller.exe (6052) lsass.exe (664) csrss.exe (536) winlogon.exe (600) dwm.exe (1100) DWM Notification Window explorer.exe (3148) Program Manager OneDrive.exe (4448) cmd.exe (5992) C:\windows\system32\cmd.exe - tlist /t conhost.exe (3120) CicMarshalWnd tlist.exe (5888) SystemSettingsAdminFlows.exe (4608) → 14 Kapitel 1 Begriffe und Werkzeuge
Die Einrückungen in der Liste zeigen die Beziehungen zwischen Eltern- und Kindprozessen. Prozesse, deren Elternprozesse nicht mehr existieren, sind nicht eingerückt (wie explorer.exe im vorstehenden Beispiel), denn selbst wenn der Großelternprozess noch vorhanden wäre, gäbe es keine Möglichkeit, diese Beziehung herauszufinden. Windows führt nur die Kennungen der Erstellerprozesse, aber keine Verbindungen zurück zu den Erstellern der Ersteller usw. Die Zahl in Klammern ist die Prozesskennung, der bei einigen Prozessen folgende Test der Titel des Fensters, das von dem Prozess angelegt wurde. Um zu beweisen, dass Windows sich nicht mehr merkt als die Kennung des Elternprozesses, führen Sie die folgenden Schritte aus: 1. Drücken Sie (Windows) + (R), geben Sie cmd ein und drücken Sie (Eingabe), um eine Eingabeaufforderung zu öffnen. 2. Geben Sie titel Parent ein, um den Titel des Fensters in Parent zu ändern. 3. Geben Sie start cmd ein, um eine zweite Eingabeaufforderung zu öffnen. 4. Geben Sie titel Child an der zweiten Eingabeaufforderung ein. 5. Geben Sie an der zweiten Eingabeaufforderung mspaint ein, um Microsoft Paint zu starten. 6. Kehren Sie wieder zu der zweiten Eingabeaufforderung zurück und geben Sie exit ein. Paint wird weiterhin ausgeführt. 7. Drücken Sie (Strg) + (Umschalt) +(Esc), um den Task-Manager zu öffnen. 8. Wenn sich der Task-Manager im Übersichtsmodus befindet, klicken Sie auf Details. 9. Klicken Sie auf die Registerkarte Prozesse. 10. Suchen Sie die Anwendung Windows Command Processor und erweitern Sie ihren Knoten. Wie im folgenden Screenshot sollten Sie jetzt den Titel Parent sehen. 11. Rechtsklicken Sie auf Windows Command Processor und wählen Sie Zu Details wechseln. → Grundprinzipien und -begriffe Kapitel 1 15
12. Rechtsklicken Sie auf diesen cmd.exe-Prozess und wählen Sie Prozessstruktur beenden. 13. Klicken Sie im Bestätigungsdialogfeld des Task-Managers auf Prozessstruktur beenden. Das Fenster mit der ersten Eingabeaufforderung verschwindet, aber das Paint-Fenster wird nach wie vor angezeigt, da es nur der Enkel des beendeten Eingabeaufforderungsprozesses ist. Da der Zwischenprozess (der Elternprozess von Paint) beendet wurde, gibt es keine Verbindung mehr zwischen dem Elternprozess und seinem Enkel. Der Process Explorer aus Sysinternals zeigt mehr Einzelheiten über Prozesse und Threads an als jedes andere verfügbare Werkzeug, weshalb wir ihn in vielen Experimenten in diesem Buch verwenden. Unter anderem gibt Ihnen Process Explorer die folgenden einzigartigen Angaben und Möglichkeiten: QQ Ein Prozesssicherheitstoken, z. B. Listen von Gruppen und Rechten und des Virtualisierungszustands QQ Hervorhebungen von Veränderungen in der Liste der Prozesse, Threads, DLLs und Handles QQ Eine Liste der Dienste in Diensthostingprozessen einschließlich Anzeigename und Beschreibung QQ Eine Liste zusätzlicher Prozessattribute wie Abschwächungsrichtlinien und deren Prozessschutzebene QQ Prozesse, die zu einem Job gehören, und Angaben zu dem Job QQ Prozesse für .NET-Anwendungen mit .NET-spezifischen Angaben wie die Aufzählung der Anwendungsdomänen, der geladenen Assemblies sowie CLR-Leistungsindikatoren QQ Prozesse für Windows Runtime (immersive Prozesse) QQ Startzeitpunkt von Prozessen und Threads QQ Vollständige Liste aller Dateien, die im Speicher zugeordnet wurden (nicht nur der DLLs) QQ Die Möglichkeit, einen Prozess oder Thread anzuhalten QQ Die Möglichkeit, einen einzelnen Thread zwangsweise zu beenden QQ Leichte Identifizierung der Prozesse, die im Laufe der Zeit die meisten CPU-Ressourcen verbrauchen Die Leistungsüberwachung kann die CPU-Nutzung eines Satzes von Prozessen darstellen, zeigt dabei aber nicht automatisch auch Prozesse an, die nach dem Start dieser Überwachungssitzung erstellt wurden. Das können Sie nur mit einer manuellen Ablaufverfolgung im binären Ausgabeformat erreichen. Des Weiteren bietet Process Explorer leichten, zentralen Zugriff auf Informationen wie die folgenden: QQ Die Prozessstruktur, wobei es möglich ist, einzelne Teile davon reduziert darzustellen QQ Offene Handles in einem Prozess, einschließlich nicht benannter Handles 16 Kapitel 1 Begriffe und Werkzeuge
QQ Liste der DLLs (und im Arbeitsspeicher zugeordneten Dateien) in einem Prozess QQ Threadaktivität in einem Prozess QQ Thread-Stacks im Benutzer- und im Kernelmodus, einschließlich der Zuordnung von Adressen zu Namen mithilfe der Dbghelp.dll, die in den Debugging Tools for Windows enthalten ist • Genauere Angabe des prozentualen CPU-Anteils mithilfe des Threadzykluszählers – eine noch bessere Darstellung der genauen CPU-Aktivität (siehe Kapitel 4, »Threads«) • Integritätsebene QQ Einzelheiten des Speicher-Managers wie Spitzenwert des festgelegten virtuellen Speichers und Grenzwerte für ausgelagerte und nicht ausgelagerte Pools des Kernelspeichers (während andere Werkzeuge nur die aktuelle Größe zeigen) Das folgende Experiment dient zur Einführung in Process Explorer. Experiment: Einzelheiten über Prozesse mit Process Explorer einsehen Laden Sie die neueste Version von Process Explorer von Sysinternals herunter und führen Sie sie aus. Das können Sie mit Standardbenutzerrechten tun, aber alternativ auch auf die ausführbare Datei rechtsklicken und Als Administrator ausführen wählen. Wenn Sie Process Explorer mit Administratorrechten ausführen, wird ein Treiber installiert, der mehr Funktionen bietet. Die folgende Beschreibung funktioniert jedoch unabhängig davon, wie Sie Process Explorer starten. Bei der ersten Ausführung von Process Explorer sollten Sie die Symbole konfigurieren, da Sie anderenfalls eine entsprechende Meldung bekommen, wenn Sie auf einen Prozess doppelklicken und die Registerkarte Threads wählen. Bei ordnungsgemäßer Konfiguration kann Process Explorer auf Symbolinformationen zugreifen, um die Symbolnamen für die Startfunktion des Threads sowie die Funktion auf seinem Aufrufstack anzuzeigen. Das ist praktisch, um zu ermitteln, was die Threads innerhalb eines Prozesses tun. Für den Zugriff auf Symbole müssen die Debugging Tools for Windows installiert sein (siehe die Beschreibung weiter hinten in diesem Kapitel). Klicken Sie auf Options, wählen Sie Configure Symbols und geben Sie den Pfad zu Dbghelp.dll im Ordner von Debugging Tools sowie einen gültigen Symbolpfad an. Wenn die Debugging Tools for Windows auf einem 64-Bit-System im Rahmen des WDK im Standardspeicherort installiert sind, können Sie beispielsweise folgende Konfiguration verwenden: → Grundprinzipien und -begriffe Kapitel 1 17
Hier wird der Symbolserver verwendet, um auf Symbole zuzugreifen, und die Symboldateien werden auf dem lokalen Computer im Ordner C:\symbols gespeichert. (Wenn Sie Speicherplatz sparen müssen, können Sie diesen Ordner auch durch einen anderen Ordner ersetzen, auch durch einen auf einem anderen Laufwerk.) Weitere Informationen darüber, wie Sie die Verwendung des Symbolservers einrichten, erhalten Sie auf https:// msdn.microsoft.com/en-us/library/windows/desktop/ee416588.aspx. Sie können den Microsoft-Symbolserver einrichten, indem Sie die Umgebungsvariable _NT_SYMBOL_PATH auf den Wert aus der vorstehenden Abbildung setzen. Mehrere Werkzeuge suchen automatisch nach dieser Variablen, darunter Process Explorer, die Debugger aus den Debugging Tools for Windows, Visual Studio u. a. Dadurch können Sie es sich ersparen, jedes Tool einzeln zu konfigurieren. Beim Start zeigt Process Explorer standardmäßig die Prozessstruktur an. Sie können den unteren Bereich erweitern, um die offenen Handles oder die im Arbeitsspeicher zugeordneten DLLs und anderen Dateien anzuzeigen (siehe auch Kapitel 5, »Speicherverwaltung«, und Kapitel 8 von Band 2). Wenn Sie den Mauszeiger über den Namen eines Prozesses halten, werden auch QuickInfos über die Prozessbefehlszeile und den Pfad eingeblendet. Bei einigen Arten von Prozessen enthält die QuickInfo auch zusätzliche Angaben, darunter die folgenden: QQ Die Dienste innerhalb eines Diensthostingprozesses (z. B. Svchost.exe) QQ Die Tasks innerhalb eines Taskhostingprozesses (z. B. TaskHostw.exe) QQ Das Ziel eines Runddl32.exe-Prozesses für Elemente der Systemsteuerung und andere Merkmale QQ Angaben zur COM-Klasse, wenn sich der Prozess in einem Dllhost.exe-Prozess befindet (auch Standard-COM+-Ersatz genannt) QQ Anbieterinformationen für WMI-Hostprozesse (Windows Management Instrumenta­ tion) wie WMIPrvSE.exe (siehe Kapitel 8 in Band 2) QQ Paketinformationen für Windows-Apps-Prozesse (Prozesse mit Windows Runtime, die weiter vorn in diesem Kapitel im Abschnitt »Die Windows-Laufzeitumgebung« vorgestellt wurden) → 18 Kapitel 1 Begriffe und Werkzeuge
Führen Sie die folgenden Schritte aus, um einige Grundfunktionen von Process Explorer kennenzulernen: 1. Machen Sie sich mit den farbigen Hervorhebungen vertraut: Prozesse für Dienste werden standardmäßig rosa hinterlegt, Ihre eigenen Prozesse dagegen blau. Diese Farben können Sie ändern, indem Sie das Dropdownmenü öffnen und Options > Configure Colors auswählen. 2. Halten Sie den Mauszeiger über den Imagenamen eines Prozesses. In einer QuickInfo wird der vollständige Pfad angezeigt. Wie zuvor erwähnt, enthält die QuickInfo bei bestimmten Arten von Prozessen weitere Angaben. 3. Klicken Sie auf der Registerkarte Process Image auf View und wählen Sie Select ­Columns, um den Imagepfad hinzuzufügen. 4. Klicken Sie auf den Kopf der Spalte Process, um die Prozesse zu sortieren. Dabei verschwindet die Strukturansicht. (Sie können nur entweder die Struktur anzeigen lassen oder eine Sortierung nach einer der Spalten durchführen.) Klicken Sie erneut auf den Kopf von Process, um eine Sortierung von Z zu A zu erreichen. Klicken Sie ein drittes Mal darauf, um wieder die Strukturansicht anzuzeigen. 5. Öffnen Sie das Menü View und deaktivieren Sie Show Processes from All Users, damit nur Ihre Prozesse angezeigt werden. → Grundprinzipien und -begriffe Kapitel 1 19
6. Klicken Sie auf das Menü Options, wählen Sie Difference Highlight Duration und ändern Sie den Wert auf drei Sekunden. Starten Sie dann einen (beliebigen) neuen Prozess. Dabei wird der neue Prozess drei Sekunden lang grün hervorgehoben. Beenden Sie den neuen Prozess. Dabei wird er drei Sekunden lang rot hervorgehoben, bevor er aus der Anzeige verschwindet. Diese Funktion ist nützlich, um sich einen Überblick über die Prozesse zu verschaffen, die auf dem System erstellt und beendet werden. 7. Doppelklicken Sie auf einen Prozess und schauen Sie sich die verschiedenen Registerkarten an, die in der Anzeige der Prozesseigenschaften zur Verfügung stehen. (In verschiedenen Experimenten in diesem Buch werden wir auf diese Registerkarten hinweisen und die darauf befindlichen Angaben erklären.) Threads Ein Thread ist eine Entität in einem Prozess, die Windows zur Ausführung einplant. Ohne einen Thread kann das Programm des Prozesses nicht ausgeführt werden. Ein Thread schließt die folgenden grundlegenden Komponenten ein: QQ Den Inhalt eines Satzes von CPU-Registern, die den Zustand des Prozessors darstellen QQ Zwei Stacks: einen für Kernel- und einen für den Benutzermodus QQ Einen privaten Speicherbereich, der als lokaler Threadspeicher (Thread-Local Storage, TLS) bezeichnet und von Teilsystemen, Laufzeitbibliotheken und DLLs verwendet wird QQ Einen eindeutigen Bezeichner, der als Threadkennung oder Thread-ID bezeichnet wird (und der zu einer internen Struktur namens Client-ID gehört; Prozess- und Thread-IDs werden aus demselben Namespace generiert, weshalb sie niemals überlappen) Darüber hinaus haben Threads manchmal auch einen eigenen Sicherheitskontext oder ein Token, das häufig von Multithread-Serveranwendungen verwendet wird, um den Sicherheitskontext der Clients anzunehmen, deren Anforderungen sie verarbeiten. Die flüchtigen Register, die Stacks und der private Speicherbereich bilden den sogenannten Kontext des Threads. Da sie sich zwischen den einzelnen Maschinenarchitekturen unterscheiden, auf denen Windows läuft, ist diese Struktur notgedrungen architekturspezifisch. Die Windows-Funktion GetThreadContext bietet Zugriff auf diese architekturspezifischen Informa­ tionen (den sogenannten CONTEXT-Block). Da an der Umschaltung der Ausführung von einem Thread zu einem anderen der Kernelscheduler beteiligt ist, kann dieser Vorgang sehr kostspielig sein, insbesondere wenn dies häufig zwischen immer denselben Threads geschieht. In Windows gibt es zwei Mechanismen, um die Kosten dafür zu senken, nämlich Fibers und UMS (User-Mode Scheduling). 20 Kapitel 1 Begriffe und Werkzeuge
Die Threads einer 32-Bit-Anwendung, die auf einer 64-Bit-Version von Windows läuft, enthalten sowohl 32- als auch 64-Bit-Kontexte, was Wow64 (Windows on Windows) dazu verwendet, die Anwendung bei Bedarf vom 32- in den 64-Bit-Modus umzuschalten. Diese Threads haben zwei Benutzerstacks und zwei CONTEXT-Blöcke. Die üblichen Windows-API-Funktionen geben stattdessen aber den 64-Bit-Kontext zurück, die Funktion Wow64GetThreadContext dagegen den 32-Bit-Kontext. Mehr Informationen über Wow64 erhalten Sie in Kapitel 8 in Band 2. Fibers Fibers ermöglichen es einer Anwendung, ihre eigenen Ausführungsthreads zu planen, anstatt sich auf den prioritätsgestützten Mechanismus von Windows zu verlassen. Sie werden oft auch als schlanke Threads (»lightweight threads«) bezeichnet. Da sie in Kernel32.dll im Benutzermodus implementiert sind, sind sie für den Kernel nicht sichtbar. Um Fibers zu nutzen, müssen Sie zunächst die Windows-Funktion ConvertThreadToFiber aufrufen, die den Thread in eine laufende Fiber umwandelt. Anschließend können Sie die neue Fiber nutzen, um mit der Funktion CreateFiber weitere Fibers zu erstellen. (Jede Fiber kann ihren eigenen Satz von Fibers haben.) Anderes als bei einem Thread jedoch beginnt die Ausführung einer Fiber erst dann, wenn sie über einen Aufruf der Funktion SwitchToFiber manuell ausgewählt wird. Die neue Fiber wird ausgeführt, bis sie mit einem Aufruf von SwitchToFiber beendet wird, der eine andere Fiber zur Ausführung auswählt. Weitere Informationen entnehmen Sie bitte der Dokumentation des Windows SDK über Fiberfunktionen. Es ist gewöhnlich nicht gut, Fibers zu verwenden, da sie für den Kernel nicht sichtbar sind. Außerdem gibt es bei ihnen Probleme durch die gemeinsame Nutzung des lokalen Threadspeichers (TLS), da in einem Thread mehrere Fibers laufen können. Es gibt zwar auch lokalen Fiberspeicher (FLS), der allerdings nicht alle Probleme der gemeinsamen Verwendung löst. An die E/A gebundene Fibers zeigen auch bei der Verwendung von FLS eine schlechte Leistung. Des Weiteren können Fiber nicht gleichzeitig auf mehreren Prozessoren laufen und sind auf kooperatives Multitasking beschränkt. In den meisten Situationen ist es besser, dem Windows-Kernel die Planung zu überlassen, indem Sie die geeigneten Threads für die vorliegende Aufgabe auswählen. UMS-Threads UMS-Threads (User-Mode Scheduling) stehen nur in den 64-Bit-Versionen von Windows zur Verfügung. Sie bieten die gleichen grundlegenden Vorteile wie Fibers, aber nur wenige von deren Nachteilen. UMS-Threads haben ihren eigenen Kernelthreadstatus und sind daher für den Kernel sichtbar, was es ermöglicht, dass mehrere UMS-Threads Ressourcen gemeinsam nutzen, darum konkurrieren und blockierende Systemaufrufe ausgeben. Wenn zwei oder mehr UMS-Threads Arbeit im Benutzermodus verrichten müssen, können sie regelmäßig den Ausführungskontext im Benutzermodus wechseln (durch Zurücktreten eines Threads zugunsten Grundprinzipien und -begriffe Kapitel 1 21
eines anderen), anstatt den Scheduler zu bemühen. Aus der Sicht des Kernels läuft weiterhin derselbe Kernelthread, weshalb sich für ihn nichts geändert hat. Führt ein UMS-Thread eine Operation durch, die einen Eintritt in den Kernel erfordert (z. B. einen Systemaufruf), dann schaltet er zu seinem eigenen Kernelmodusthread um (was als direkter Kontextwechsel bezeichnet wird). Es ist zwar immer noch nicht möglich, UMS-Threads gleichzeitig auf mehreren Prozessoren auszuführen, doch zumindest folgen sie einem präemptiven und nicht vollständig kooperativen Modell. Threads haben zwar ihren eigenen Ausführungskontext, doch alle Threads in einem Prozess teilen sich seinen virtuellen Adressraum (neben den anderen Ressourcen des Prozesses) und haben vollständigen Lese- und Schreibzugriff darauf. Allerdings ist es nicht möglich, dass ein Thread versehentlich auf den Adressraum eines anderen Prozesses verweist, sofern nicht dieser andere Prozess Teile seines privaten Adressraums als freigegebenen Speicherbereich verfügbar macht (was in der Windows-API als Dateizuordnungsobjekt [File Mapping Object] bezeichnet wird) oder ein Prozess das Recht hat, andere Prozesse zur Verwendung von prozessübergreifenden Speicherfunktionen zu öffnen, z. B. ReadProcessMemory und WriteProcessMemory. (Dieses Recht kann ein Prozess, der in demselben Benutzerkonto und nicht innerhalb eines Anwendungscontainers oder einer anderen Art von Sandbox läuft, standardmäßig erwerben, wenn der Zielprozess keine entsprechenden Schutzvorkehrungen aufweist.) Neben dem privaten Adressraum und einem oder mehr Threads verfügt jeder Prozess auch über einen Sicherheitskontext und einen Satz von offenen Handles für Kernelobjekte wie Dateien, freigegebene Speicherbereiche oder Synchronisierungsobjekte wie Mutexe, Ereignisse oder Semaphoren (siehe Abb. 1–2). Zugriffstoken Prozess VAD VAD VAD Handletabelle Objekt Objekt Thread Thread Thread Zugriffstoken Abbildung 1–2 Ein Prozess und seine Ressourcen Der Sicherheitskontext eines Prozesses wird jeweils in einem Objekt namens Zugriffstoken gespeichert. Es enthält die Sicherheitsidentifizierung und die Anmeldeinformationen für den Prozess. Standardmäßig haben Threads keine eigenen Zugriffstokens. Sie können allerdings eines erwerben, sodass einzelne Threads die Identität des Sicherheitskontextes eines anderes Prozesses annehmen können – auch von Prozessen auf einem anderen Windows-System –, ohne andere Threads in dem Prozess zu beeinträchtigen. (Weitere Einzelheiten zur Prozess- und Threadsicherheit erhalten Sie in Kapitel 7.) 22 Kapitel 1 Begriffe und Werkzeuge
Die VADs (Virtual Address Descriptors) sind Datenstrukturen, die der Speicher-Manager nutzt, um sich über die vom Prozess verwendeten virtuellen Adressen auf dem neuesten Stand zu halten. Diese Datenstrukturen werden in Kapitel 5 ausführlicher beschrieben. Jobs In Windows gibt es Jobs als Erweiterung des Prozessmodells. Der Hauptzweck von Jobs besteht darin, die Verwaltung und Bearbeitung von Prozessgruppen als eine Einheit zu ermöglichen. Diese Objekte erlauben es, bestimmte Attribute zu steuern, und bieten Grenzwerte für die mit ihnen verknüpften Prozesse. Sie zeichnen auch grundlegende Buchführungsinformationen für alle verknüpften Prozesse auf, sowohl die aktuellen als auch diejenigen, die bereits beendet wurden. In gewisser Hinsicht bilden Jobs einen Ersatz für den fehlenden strukturierten Prozessbaum in Windows. In manchen Aspekten sind sie jedoch leistungsfähiger als ein Prozessbaum nach Art von UNIX. Der Process Explorer kann die von einem Job verwalteten Prozesse anzeigen (standardmäßig in Braun), aber diese Funktion müssen Sie erst aktivieren. Dazu öffnen Sie das Menü Options und wählen Configure Colors. Auf den Eigenschaftenseite solcher Prozesse gibt es außerdem die zusätzliche Registerkarte Jobs, auf der Sie Informationen über das zugehörige Jobobjekt finden. Weitere Informationen über die interne Struktur von Prozessen und Jobs erhalten Sie in Kapitel 3. Threads und Algorithmen zu ihrer Planung sind Thema von Kapitel 4. Virtueller Arbeitsspeicher Windows richtet ein virtuelles Speichersystem auf der Grundlage eines linearen Adressraums ein, das jedem Prozess die Illusion gibt, über einen riesigen, privaten Adressraum für sich selbst zu verfügen. Der virtuelle Speicher bietet eine logische Sicht des Arbeitsspeichers, die nicht unbedingt mit dem physischen Aufbau übereinstimmen muss. Zur Laufzeit bildet der Speicher-Manager, unterstützt von der Hardware, die virtuellen Adressen auf die physischen Adressen ab (oder ordnet sie ihnen zu), an denen die Daten tatsächlich gespeichert werden. Durch Schutzvorkehrungen und durch Steuern dieser Zuordnung sorgt das Betriebssystem dafür, dass einzelne Prozesse nicht kollidieren und gegenseitig Betriebssystemdaten überschreiben. Da der physische Arbeitsspeicher auf den meisten Systemen viel kleiner ist als der gesamte virtuelle Arbeitsspeicher, der von den laufenden Prozessen genutzt wird, lagert der Speicher-Manager einige Speicherinhalte auf die Festplatte aus. Wenn ein Thread auf eine virtuelle Adresse zugreift, die ausgelagert wurde, lädt der Speicher-Manager die Informationen von der Festplatte zurück in den Arbeitsspeicher. Um die Auslagerung nutzen zu können, ist keinerlei Anpassung der Anwendungen erforderlichen, da die Hardware dem Speicher-Manager die Auslagerung ermöglicht, ohne dass die Prozesse oder Threads das wissen oder unterstützen müssen. Abb. 1–3 zeigt zwei Prozesse, die virtuellen Arbeitsspeicher nutzen. Teilweise ist er auf den physischen Speicher (RAM) abgebildet und teilweise ausgelagert. Beachten Sie, dass zusammenhängende virtuelle Spei- Grundprinzipien und -begriffe Kapitel 1 23
cherbereiche auf nicht zusammenhängende Bereiche im physischen Speicher abgebildet werden können. Diese Bereiche werden als Seiten bezeichnet und haben eine Standardgröße von 4 KB. Virtueller Arbeitsspeicher Physischer Arbeitsspeicher Prozess A Virtueller Arbeitsspeicher Prozess B Festplatte Abbildung 1–3 Zuweisung von virtuellem zu physischem Speicher mit Auslagerung Die Größe des virtuellen Adressraums hängt von der Hardwareplattform ab. Auf 32-Bit-x86-­ Systemen hat der gesamte virtuelle Adressraum eine theoretische Maximalgröße von 4 GB. Standardmäßig weist Windows die untere Hälfte dieses Adressraums (Adressen 0x00000000 bis 0x7FFFFFFF) für den privaten Speicher der Prozesse zu und die obere Hälfte (0x80000000 bis 0xFFFFFFFF) für seinen eigenen geschützten Betriebssystemspeicher. Die Zuordnungen der unteren Hälfte ändern sich mit dem jeweils ausgeführten Prozess, aber die Zuordnungen der oberen Hälfte (zumindest der Großteil davon) besteht immer aus dem virtuellen Speicher für das Betriebssystem. In Windows können auch Bootoptionen eingesetzt werden, z. B. der Qualifizierer increaseuserva aus der Bootkonfigurationsdatenbank (siehe Kapitel 5), der Prozessen für besonders gekennzeichnete Programme die Möglichkeit einräumt, bis zu 3 GB privaten Adressraum zu verwenden, wobei für das Betriebssystem 1 GB übrig bleibt. (»Besonders gekennzeichnet« bedeutet, dass im Header des ausführbaren Images das Flag für großen Adressraum gesetzt ist.) Diese Option ermöglicht Anwendungen wie Datenbankservern, größere Teile der Datenbank im Prozessadressraum zu belassen, was die Notwendigkeit der Auslagerung von Teilsichten verringert und damit die Leistung erhöht (wobei jedoch der Verlust von 1 GB für das System in manchen Fällen zu deutlichen systemweiten Leistungsverlusten führen kann). Abb. 1–4 zeigt die beiden typischen Adressraumanordnungen in 32-Bit-Versionen von Windows. (Die Option increaseuserva ermöglicht ausführbaren Images mit dem Flag für großen Adressraum, einen Bereich mit einer beliebigen Größe zwischen 2 bis 3 GB zu nutzen.) 24 Kapitel 1 Begriffe und Werkzeuge
Hohe Adressen 32 Bit (Standard) 2 GB Systemraum 2 GB Benutzerprozessraum 32 Bit (optional) 1 GB Systemraum 3 GB Benutzerprozessraum Niedrige Adressen Abbildung 1–4 Typische Adressraumaufteilungen für 32-Bit-Windows 3 GB ist zwar schon besser als 2 GB, aber das ist immer noch nicht genügend virtueller Adressraum zur Zuordnung von sehr großen Datenbanken (von mehreren Gigabytes). Um dieses Problem auf 32-Bit-Systemen anzugehen, verwendet Windows den sogenannten AWE-Mechanismus (Address Windowing Extension), der es einer 32-Bit-Anwendung möglich macht, bis zu 64 GB physischen Arbeitsspeicher zuzuweisen und dann Sichten oder Fenster auf seinen 2 GB großen virtuellen Adressenraum abzubilden. Diese Vorgehensweise bürdet die Abbildung des virtuellen Adressraums auf den physischen zwar dem Entwickler auf, erfüllt aber das Bedürfnis, mehr physischen Arbeitsspeicher direkt zuzuweisen, als in einem 32-Bit-Prozessraum abgebildet werden kann. In 64-Bit-Windows steht den Prozessen ein viel größerer Adressraum zur Verfügung, nämlich 128 TB auf Windows 8.1, Server 2012 R2 und höher. Abb. 1–5 zeigt eine vereinfachte Darstellung des 64-Bit-Systemadressraums. (Eine ausführliche Beschreibung finden Sie in Kapitel 5.) Beachten Sie, dass die angegebenen Größen nicht die von der Architektur vorgegebenen Grenzwerte darstellen. 64 Bit Adressraum entsprechen 264 oder 16 EB (wobei 1 EB gleich 1024 PB oder 1.048.576 TB ist), doch die aktuelle 64-Bit-Hardware beschränkt dies auf kleinere Werte. Der als »nicht zugeordnet« gekennzeichnete Bereich in Abb. 1–5 ist viel größer als der Bereich, der zugeordnet sein kann (auf Windows 8 ungefähr eine Million Mal größer). Die Darstellung ist also bei Weitem nicht maßstabsgerecht. 64 Bit (Windows 8) 64 Bit (Windows 8.1+) Hohe Adressen 8 TB Systemraum Nicht zugeordnet Niedrige Adressen Abbildung 1–5 8 TB Benutzerprozessraum 128 TB Systemraum Nicht zugeordnet 128 TB Benutzerprozessraum Adressraumaufteilungen für 64-Bit-Windows Die Implementierung des Speicher-Managers, die Adressabbildung und die Verwaltung des physischen Arbeitsspeichers durch Windows werden in Kapitel 5 ausführlicher beschrieben. Grundprinzipien und -begriffe Kapitel 1 25
Kernel- und Benutzermodus Um zu verhindern, dass Benutzeranwendungen auf kritische Betriebssystemdaten zugreifen und sie vielleicht sogar ändern, verwendet Windows zwei verschiedene Prozessorzugriffsmodi (auch wenn der Prozessor, auf denen Windows läuft, mehr als zwei unterstützen kann), nämlich den Benutzermodus und den Kernelmodus. Benutzeranwendungen laufen im Benutzermodus, Betriebssystemcode (wie Systemdienste und Gerätetreiber) dagegen im Kernelmodus, der Zugriff auf den gesamten Systemspeicher und alle CPU-Anweisungen gewährt. Zur Unterscheidung dieser Modi werden bei manchen Prozessoren die Begriffe Code Privilege Level und Ring Level und bei anderen Supervisor Mode und Application Mode verwendet. Unabhängig von der Bezeichnung gestatten sie dem Betriebssystemkernel höhere Rechte als Benutzeranwendungen, wodurch Betriebssystementwickler die erforderliche Grundlage bekommen, um sicherzustellen, dass eine nicht ordnungsgemäß funktionierende Anwendung die Stabilität des Gesamtsystems nicht beeinträchtigen kann. Die Architekturen von x86- und x64-Prozessoren definieren vier Rechteebenen (Ringe), um Systemcode und -daten gegen versehentliches oder böswilliges Überschreiben durch Code mit geringeren Rechten zu schützen. Windows verwendet Rechteebene 0 (Ring 0) für den Kernel- und Rechteebene 3 (Ring 3) für den Benutzermodus. Der Grund dafür, dass Windows nur zwei Ebenen nutzt, besteht darin, dass einige Hardwarearchitekturen, wie ARM oder früher MIPS/Alpha, nur zwei implementiert hatten. Die Entscheidung für diesen kleinsten gemeinsamen Nenner ermöglichte eine effizientere und portierbare Architektur, insbesondere da die anderen Ringebenen von x86/x64 nicht die gleichen Garantien bieten wie die Unterscheidung zwischen Ring 0 und Ring 3. Alle Windows-Prozesse haben zwar jeweils ihren eigenen privaten Arbeitsspeicher, doch der Kernelmoduscode des Betriebssystems und der Gerätetreiber teilen sich denselben virtuellen Adressraum. Jede Seite im virtuellen Speicher wird mit dem Zugriffsmodus gekennzeichnet, den der Prozessor haben muss, um sie zu lesen bzw. zu schreiben. Seiten im Systemadressraum sind nur vom Kernelmodus aus zugänglich, Seiten im Benutzeradressraum dagegen vom Benutzer- und vom Kernelmodus aus. Schreibgeschützte Seiten (z. B. solche, die statische Daten enthalten) sind in keinem Modus schreibbar. Auf Prozessoren mit Ausführungsschutz für den Arbeitsspeicher kann Windows außerdem Seiten mit Daten als nicht ausführbar kennzeichnen und dadurch die unabsichtliche oder böswillige Codeausführung in Datenbereichen verhindern (wenn die Funktion der Datenausführungsverhinderung eingeschaltet ist). Windows bietet keinen Schutz für privaten les- und schreibbaren Systemspeicher, der von Komponenten im Kernelmodus genutzt wird. Anders ausgedrückt, im Kernelmodus hat der Betriebssystem- und Gerätetreibercode vollständigen Zugriff auf den Systemarbeitsspeicher und kann die Windows-Sicherheit umgehen, um auf Objekte zuzugreifen. Da der Großteil des Windows-Betriebssystemcodes im Kernelmodus läuft, ist es unverzichtbar, die im Kernelmodus ausgeführten Komponenten sorgfältig zu entwerfen und zu testen, damit sie die Systemsicherheit nicht gefährden und keine Instabilitäten des Systems hervorrufen. 26 Kapitel 1 Begriffe und Werkzeuge
Aufgrund des mangelnden Schutzes ist es auch wichtig, beim Laden von Drittanbieter-Gerätetreibern besonders achtsam zu sein, insbesondere wenn diese nicht signiert sind, denn wenn sich der Treiber erst einmal im Kernelmodus befindet, hat er kompletten Zugriff auf sämtliche Betriebssystemdaten. Dieses Risiko war einer der Gründe für die Einführung eines Mechanismus zur Treibersignierung in Windows 2000. Dabei wird bei dem Versuch, einen nicht signierten Plug-&-Play-Treiber hinzuzufügen, der Benutzer gewarnt und bei entsprechender Einstellung der Vorgang blockiert. (Weitere Informationen über Treibersignierung erhalten Sie in Kapitel 6, »Das E/A-System«.) Andere Arten von Treibern sind davon jedoch nicht betroffen. Die Treiberüberprüfung hilft Treiberautoren außerdem, Bugs zu finden, die die Sicherheit oder Zuverlässigkeit gefährden, z. B. Pufferüberläufe und Speicherlecks. (Die Treiberüberprüfung wird ebenfalls in Kapitel 6 erläutert.) In der 64-Bit- und der ARM-Version von Windows 8.1 schreibt die Richtlinie zur Kernelmodus-Codesignierung (KMCS) vor, dass alle Gerätetreiber (nicht nur Plug-&-Play-Treiber) mit einem kryptografischen Schlüssel von einer der namhaften Zertifizierungsstellen signiert sein müssen. Dadurch können die Benutzer die Installation eines nicht signierten Treibers nicht erzwingen, nicht einmal mit Administratorrechten. Die Einschränkung kann einmalig manuell deaktiviert werden, um Treiber selbst zu signieren und zu testen. Dabei wird auf dem Desktophintergrund jedoch das Wasserzeichen »Test Mode« angezeigt, und es werden bestimmte Funktionen der digitalen Rechteverwaltung (DRM) ausgeschaltet. In Windows 10 hat Microsoft eine noch stärkere Änderung umgesetzt, die ein Jahr nach der Veröffentlichung im Rahmen des Anniversary Updates im Juli durchgesetzt wurde (Version 1607). Seit diesem Zeitpunkt müssen alle neuen Windows 10-Treiber von einer der beiden akzeptierten Zertifizierungsstellen mit einem SHA-2-EV-Hardwarezertifikat (Extended Validation) signiert werden. Das reguläre dateigestützte SHA-1-Zertifikat, das von 20 Stellen ausgegeben wird, reicht also nicht mehr aus. Nach der EV-Signierung muss der Hardwaretreiber außerdem durch das Systemgeräteportal (SysDev) zur Bescheinigung eingereicht werden, bei der der Treiber eine Microsoft-Signatur erhält. Der Kernel signiert nun nur noch von Microsoft signierte Windows 10-Treiber. Außer dem zuvor erwähnten Testmodus gibt es keine Ausnahmen. Treiber, die vor dem Veröffentlichungsdatum von Windows 10 (Juli 2015) signiert wurden, können vorläufig jedoch weiterhin mit der regulären Signatur geladen werden. Die strengsten Anforderungen stellt zurzeit Windows Server 2016. Die bereits genannte EV-Signierung ist nach wie vor erforderlich, aber hier reicht die anschließende reine Bescheinigungssignierung nicht mehr aus. Damit ein Windows 10-Treiber auf einem Serversystem geladen werden kann, muss er die strenge WHQL-Zertifizierung (Windows Hardware Quality Labs) im Rahmen des Hardware Compatibility Kit (HCK) durchlaufen und zur formalen Evaluierung eingereicht werden. Nur Treiber mit WHQL-Signierung – die Systemadministratoren gewisse Kompatibilitäts-, Sicherheits-, Leistungs- und Stabilitätsgarantien gibt – dürfen auf solchen Systemen geladen werden. Unter dem Strich sollte die Beschränkung der Drittanbietertreiber, die in den Kernelmodusspeicher geladen werden dürfen, für erhebliche Verbesserungen bei der Stabilität und Sicherheit sorgen. Bei einzelnen Hersteller-, Plattform- und sogar Unternehmenskonfigurationen von Win­ dows können diese Signierungsrichtlinien angepasst werden, z. B. durch die DeviceGuard-­ Technologie, die wir weiter hinten in diesem Kapitel im Abschnitt »Hypervisor« sowie in Kapitel 7 noch kurz erläutern werden. Damit kann ein Unternehmen WHQL-Signaturen auch Grundprinzipien und -begriffe Kapitel 1 27
auf Windows 10-Clientsystemen verlangen oder diese Anforderung auf Windows Server 2016-­Systemen aussetzen. Wie Sie in Kapitel 2, »Systemarchitektur«, sehen werden, wechseln Benutzeranwendungen vom Benutzer- in den Kernelmodus, wenn sie einen Systemdienst aufrufen. So muss beispielsweise die Windows-Funktion ReadFile irgendwann die interne Windows-Routine aufrufen, die sich eigentlich um das Lesen der Daten in einer Datei kümmert. Da diese Routine auf interne Systemdatenstrukturen zugreift, muss sie im Kernelmodus laufen. Durch eine besondere Prozessoranweisung wird der Wechsel vom Benutzer- in den Kernelmodus ausgelöst, wobei der Prozessor in den Zuteilungscode für Systemdienste im Kernel eintritt. Dieser Code wiederum ruft die entsprechende interne Funktion in Ntoskrnl.exe oder Win32k.sys auf. Vor der Rückgabe der Steuerung an den Benutzerthread wird der Prozessormodus wieder zurück in den Benutzermodus geschaltet. Dadurch schützt das Betriebssystem sich selbst und seine Daten vor der Verwendung und Bearbeitung durch Benutzerprozesse. Der Wechsel vom Benutzer- in den Kernelmodus (und umgekehrt) wirkt sich nicht per se auf die Threadplanung aus. Ein Moduswechsel ist kein Kontextwechsel. Nähere Einzelheiten zur Systemdienstzuteilung finden Sie in Kapitel 2. Es ist daher normal, wenn ein Benutzerthread manchmal im Benutzer- und manchmal im Kernelmodus ausgeführt wird. Da der Großteil des Grafik- und Fenstersystems auch im Kernelmodus läuft, verbringen grafikintensive Anwendungen sogar mehr Zeit im Kernel- als im Benutzermodus. Das können Sie leicht beobachten, indem Sie eine grafikintensive Anwendung wie Microsoft Paint ausführen und die zeitliche Aufteilung zwischen Benutzer- und Kernelmodus anhand der in Tabelle 1–3 aufgeführten Leistungsindikatoren messen. Anspruchsvollere Anwendungen können auch modernere Technologien wie Direct2D und DirectComposition nutzen, die Massenberechnungen im Benutzermodus durchführen und nur die Oberflächenrohdaten an den Kernel senden, was die erforderliche Zeit für den Wechsel zwischen Benutzer- und Kernelmodus verringert. Objekt: Indikator Bedeutung Prozessor: Privilegierte Zeit (%) Prozentualer Anteil der Zeit, in der eine einzelne CPU (oder alle CPUs) während eines gegebenen Zeitraums im Kernelmodus lief Prozessor: Benutzerzeit (%) Prozentualer Anteil der Zeit, in der eine einzelne CPU (oder alle CPUs) während eines gegebenen Zeitraums im Benutzermodus lief Prozess: Privilegierte Zeit (%) Prozentualer Anteil der Zeit, in der die Threads in einem Prozess während eines gegebenen Zeitraums im Kernelmodus ausgeführt wurden Prozess: Benutzerzeit (%) Prozentualer Anteil der Zeit, in der die Threads in einem Prozess während eines gegebenen Zeitraums im Benutzermodus ausgeführt wurden Thread: Privilegierte Zeit (%) Prozentualer Anteil der Zeit, in der ein Thread während eines gegebenen Zeitraums im Kernelmodus ausgeführt wurde Thread: Benutzerzeit (%) Prozentualer Anteil der Zeit, in der ein Thread während eines gegebenen Zeitraums im Benutzermodus ausgeführt wurde Tabelle 1–3 28 Leistungsindikatoren im Zusammenhang mit Benutzer- und Kernelmodus Kapitel 1 Begriffe und Werkzeuge
Experiment: Kernel- und Benutzermodus Mit der Leistungsüberwachung können Sie sich ansehen, wie viel Zeit Ihr System im Kernelund im Benutzermodus jeweils verbringt. Gehen Sie dazu wie folgt vor: 1. Öffnen Sie das Startmenü und geben Sie Run Performance Monitor ein, um die Leistungsüberwachung auszuführen. (Der vollständige Befehl sollte schon angezeigt werden, bevor Sie die Eingabe abgeschlossen haben.) 2. Wählen Sie in der Struktur auf der linken Seite die Leistungsüberwachung unter Leistungs-/Überwachungstools aus. 3. Um den Standardindikator für die CPU-Gesamtzeit zu entfernen, klicken Sie auf die Schaltfläche Löschen in der Systemleiste oder drücken die Taste (Entf). 4. Klicken Sie in der Symbolleiste auf die Schaltfläche Hinzufügen (die mit dem großen Pluszeichen). 5. Erweitern Sie den Indikatorbereich Prozessor, klicken Sie auf Privilegierte Zeit (%) und bei gedrückter (Strg)-Taste auf Benutzerzeit (%). 6. Klicken Sie auf Hinzufügen und dann auf OK. 7. Öffnen Sie eine Eingabeaufforderung und geben Sie dir\\%computername\c$ /s ein, um einen Verzeichnisscan von Laufwerk C zu starten. 8. Schließen Sie das Tool, wenn Sie fertig sind. → Grundprinzipien und -begriffe Kapitel 1 29
Sie können sich diesen Überblick auch mit dem Task-Manager verschaffen. Klicken Sie dort einfach auf die Registerkarte Leistung und dann auf das CPU-Diagramm und wählen Sie Kernel-Zeiten anzeigen. Die CPU-Leistungsanzeige zeigt jetzt in einem dunkleren Hellblau die CPU-Nutzung im Kernelmodus. Um zu sehen, wie die Verteilung zwischen Benutzer- und Kernelmodus bei einzelnen Prozessen aussieht, führen Sie das Tool erneut aus, fügen aber die Prozessindikatoren Benutzerzeit (%) und Privilegierte Zeit (%) für die gewünschten Prozesse im System hinzu: 1. Starten Sie die Leistungsüberwachung ggf. neu. (Falls sie noch läuft, rechtsklicken Sie auf den Grafikbereich und wählen Sie Alle Leistungsindikatoren entfernen, um mit einer leeren Anzeige beginnen zu können.) 2. Klicken Sie in der Symbolleiste auf die Schaltfläche mit dem Pluszeichen. 3. Erweitern Sie im Bereich der verfügbaren Indikatoren den Abschnitt Prozess. 4. Wählen Sie die Indikatoren Benutzerzeit (%) und Privilegierte Zeit (%) aus. 5. Wählen Sie im Feld Instanz einige Prozesse aus (z. B. mmc, csrss und Leerlauf). 6. Klicken Sie auf Hinzufügen und dann auf OK. 7. Bewegen Sie den Mauszeiger rasch hin und her. 8. Drücken Sie (Strg) + (H), um den Hervorhebungsmodus einzuschalten. Dadurch wird der zurzeit ausgewählte Indikator schwarz hervorgehoben. 9. Scrollen Sie durch die Liste am unteren Rand der Anzeige, um die Prozesse zu erkennen, die ausgeführt wurden, als Sie die Maus bewegt haben. Achten Sie darauf, ob sie im Benutzer- oder im Kernelmodus ausgeführt wurden. Bei der Bewegung der Maus sollten Sie in der Spalte Instanz für den Prozess mmc eine Zunahme der Zeit sowohl im Kernel- als auch im Benutzermodus sehen, da der Prozess Anwendungscode im Benutzermodus ausführt und Windows-Funktionen aufruft, die im Kernelmodus laufen. Bei der Bewegung der Maus werden Sie auch eine Threadaktivität im Kernelmodus für den Prozess csrss feststellen. Ursache dafür ist der Kernelmodusthread des Windows-Teilsystems für Roheingaben von der Tastatur und die Maus, der mit diesem Prozess verknüpft ist. (Mehr über Systemthreads und Teilsysteme erfahren Sie in Kapitel 2.) Der Leerlaufprozess, der fast 100 % seiner Zeit im Kernelmodus verbringt, ist kein echter Prozess, sondern stellt nur eine Möglichkeit dar, die Leerlaufzyklen der CPU zu berücksichtigen. Wenn Windows nichts zu tun hat, so tut es das dieser Anzeige zufolge im Kernelmodus. Hypervisor Jüngste Entwicklungen bei den Anwendungs- und Softwaremodellen wie die Einführung von Clouddiensten und die weite Verbreitung von IoT-Geräten haben dazu geführt, dass Betriebssystem- und Hardwarehersteller effizientere Möglichkeiten finden müssen, um virtuelle Gastbetriebssysteme auf der Hosthardware eines Computers auszuführen, entweder um mehrere Systeme auf einer Serverfarm unterzubringen und 100 isolierte Websites auf einem einzigen Server auszuführen oder um Entwicklern zu erlauben, Dutzende von verschiedenen Betriebs- 30 Kapitel 1 Begriffe und Werkzeuge
systemvarianten zu testen, ohne spezialisierte Hardware dafür kaufen zu müssen. Der Bedarf für schnelle, effiziente und sichere Virtualisierung hat neue Modelle des Computerwesens und neue Ansichten über Software gefördert. Heute wird besondere Software – beispielsweise Docker, das in Windows 10 und Windows Server 2016 unterstützt wird – in Containern ausgeführt, um komplett isolierte virtuelle Maschinen zu bilden, die ausschließlich dafür da sind, einen einzelnen Anwendungsstack oder ein Framework auszuführen, was die Grenzen zwischen Gast und Host mehr und mehr verschwimmen lässt. Um solche Virtualisierungsdienste anzubieten, nutzen fast alle modernen Lösungen einen Hypervisor. Dabei handelt es sich um eine spezialisierte und mit hohen Rechten ausgestattete Komponente, die die Virtualisierung und Isolierung aller Ressourcen des Computers erlaubt – vom virtuellen und physischen Arbeitsspeicher über Geräteinterrupts bis zu PCI- und USB-Geräten. Ein Beispiel für einen Hypervisor ist Hyper-V, das die Hyper-V-Clientfunktionen von Windows 8.1 und höher ermöglicht. Konkurrenzprodukte wie Xen, KVM, VMware und VirtualBox haben ihre eigenen Hypervisors mit ihren jeweiligen Stärken und Schwächen. Da ein Hypervisor hohe Rechte und stärkeren Zugriff als der Kernel selbst hat, bietet er einen erheblichen Vorteil gegenüber der reinen Möglichkeit, mehrere Gastinstanzen anderer Betriebssysteme auszuführen: Er kann einzelne Hostinstanzen schützen und überwachen, um mehr Garantien zu geben, als es der Kernel vermag. In Windows 10 nutzt Microsoft jetzt den Hypervisor Hyper-V, um die neuen VBS-Dienste (Virtualization-Based Security) anzubieten: QQ DeviceGuard Bietet Hypervisor-Codeintegrität (HVCI) für eine stärkere Codesignierungsgarantie als nur mit KMCS und ermöglicht die Anpassung der Signaturrichtlinie von Windows sowohl für Benutzer- als auch für Kernelmoduscode. QQ HyperGuard visors. Schützt wichtige Datenstrukturen und Code des Kernels und des Hyper­ QQ CredentialGuard Verhindert den nicht autorisierten Zugriff auf Anmeldeinformationen und Geheimnisse für Domänenkonten in Kombination mit biometrischer Sicherheit. QQ ApplicationGuard Stellt eine noch stärkere Sandbox für den Browser Microsoft Edge zur Verfügung. QQ HostGuardian und ShieldedFabric Sie nutzen virtuelles TPM (v-TPM), um eine virtuelle Maschine vor der Infrastruktur zu schützen, auf der sie ausgeführt wird. Außerdem ermöglicht Hyper-V einige wichtige Vorsorgemaßnahmen des Kernels gegen Exploits und ähnliche Angriffe. Der Hauptvorteil all dieser Technologien besteht darin, dass sie anders als vorherige kernelgestützte Sicherheitsvorkehrungen nicht anfällig für bösartige oder schlecht geschriebene Treiber sind, ob signiert oder nicht. Das gibt ihnen eine hohe Widerstandsfähigkeit gegen die anspruchsvollen modernen Angriffe. Möglich wird dies durch die Implementierung von virtuellen Vertrauensebenen (Virtual Trust Levels, VTL) durch den Hypervisor. Da das normale Betriebssystem und seine Komponenten in einem weniger privilegierten Modus laufen (VTL 0), die VBS-Technologien aber auf VTL 1 (mit höheren Rechten), können sie durch Kernelmoduscode nicht beeinträchtigt werden. Der Code verbleibt daher im Rechteraum von VTL 0. In dieser Hinsicht können Sie sich VTLs als orthogonal zu den Rechteebenen des Prozessors vorstellen: Kernel- und Benutzermodus existieren innerhalb der einzelnen VTLs und der Hypervisor verwaltete die Rechte über die VTLs hinweg. Kapitel 2 behandelt weitere Grundprinzipien und -begriffe Kapitel 1 31
Einzelheiten über hypervisorunterstützte Architektur. In Kapitel 7 werden die VBS-Sicherheitsmechanismen ausführlich erläutert. Firmware Windows-Komponenten verlassen sich zunehmend auf die Sicherheit des Betriebssystems und seines Kernels, wobei sich Letzterer wiederum auf den Schutz des Hypervisors verlässt. Es stellt sich jedoch die Frage, was dafür sorgt, dass diese Komponenten sicher geladen und ihre Inhalt authentifiziert werden können. Das ist gewöhnlich die Aufgabe des Bootloaders, aber auch er benötigt irgendeine Form von Authentifizierungsprüfung. Auf diese Weise entsteht eine immer kompliziertere Vertrauenshierarchie. Was also bildet den Anfang dieser Vertrauenskette, um einen ungestörten Startvorgang zu garantieren? In modernen Systemen mit Windows 8 und höher fällt dies in die Zuständigkeit der Systemfirmware, die auf zertifizierten Systemen auf UEFI basieren muss. Als Teil des von Windows vorgeschriebenen UEFI-Standards (UEFI 2.3.1b; siehe http://www.uefi.org) muss eine sichere Bootimplementierung mit starken Garantien und Anforderungen zur Signierung von bootrelevanter Software vorhanden sein. Aufgrund dieser Verifizierung besteht die Garantie, dass Windows-Komponenten vom Anfang des Vorgangs an sicher geladen werden. Außerdem können Technologien wie TPM (Trusted Platform Module) den Prozess messen, um eine Bescheinigung (sowohl lokal als auch im Netzwerk) zu bieten. Durch Branchenpartnerschaften unterhält Microsoft eine Whitelist und eine Blacklist von UEFI-sicheren Bootkomponenten für den Fall von Bootsoftwarefehlern oder -angriffen. In Windows-Updates sind jetzt auch Firm­ wareaktualisierungen enthalten. Wir werden zwar erst in Kapitel 11, »Starten und Herunterfahren«, von Band 2 wieder auf Firmware kommen, doch es ist wichtig, schon hier auf die Bedeutung hinzuweisen, die sie durch ihre Garantien für die moderne Windows-Architektur bietet. Terminaldienste und mehrere Sitzungen Terminaldienste bezieht sich auf die Unterstützung, die Windows für mehrere interaktive Benutzersitzungen auf einem einzelnen System bietet. Mit den Windows-Terminaldiensten kann ein Remotebenutzer eine Sitzung auf einem anderen Computer einrichten, sich anmelden und Anwendungen auf dem Server ausführen. Der Server überträgt die grafische Benutzeroberfläche (sowie andere auswählbare Ressourcen wie Audio und die Zwischenablage) an den Client, und der Client überträgt die Benutzereingabe zurück an den Server. (Ähnlich wie das X-Window-System erlaubt Windows die Ausführung einzelner Anwendungen auf einem Serversystem so, dass nur deren Anzeige und nicht der gesamte Desktop an den Client übertragen wird.) Die erste Sitzung wird als Dienstesitzung oder Sitzung 0 angesehen und enthält Prozesse, die Systemdienste beherbergen (mehr darüber in Kapitel 9 von Band 2). Die erste Anmeldesitzung an der physischen Konsole des Computers ist Sitzung 1. Weitere Sitzungen können durch die Verwendung eines Programms für Remotedesktopverbindungen (Mstsc.exe) oder den schnellen Benutzerwechsel erstellt werden. Die Clientausgaben von Windows erlauben einem einzelnen Remotebenutzer, Verbindung mit dem Computer aufzunehmen, aber wenn schon jemand an der Konsole angemeldet ist, so 32 Kapitel 1 Begriffe und Werkzeuge
ist die Arbeitsstation gesperrt. Das bedeutet, dass System kann entweder lokal oder über das Netzwerk genutzt werden, aber nicht beides zugleich. Windows-Ausgaben mit Windows Media Center gestatten eine interaktive und bis zu vier Windows Media Center Extender-Sitzungen. Windows-Serversysteme ermöglichen zwei Remoteverbindungen zur selben Zeit. Das dient zur Erleichterung der Remoteverwaltung, also etwa die Verbindung von Verwaltungswerkzeugen, für deren Nutzung sie an dem zu verwaltenden Computer angemeldet sein müssen. Außerdem unterstützen diese Systeme bis zu zwei Remotesitzungen, falls sie entsprechend lizenziert und als Terminalserver eingerichtet sind. Alle Clientausgaben von Windows ermöglichen mehrere Sitzungen, die lokal durch den schnellen Benutzerwechsel erstellt werden. Von diesen Sitzungen kann jedoch immer nur eine auf einmal ausgeführt werden. Anstatt sich abzumelden, kann ein Benutzer auch nur seine Sitzung trennen. Dazu klickt er auf die Startschaltfläche und dann auf den aktuellen Benutzer und wählt in dem daraufhin erscheinenden Untermenü Benutzer wechseln. Er kann auch (Windows) + (L) drücken und dann unten links auf einen anderen Benutzer klicken. In jedem Fall bleibt die aktuelle Sitzung – d. h., die Prozesse, die darin laufen, und alle sitzungsweiten Datenstrukturen, die sie beschreiben – im System aktiv, während das System zum Hauptanmeldebildschirm zurückkehrt (falls es sich nicht bereits dort befindet). Wenn sich ein neuer Benutzer anmeldet, wird eine neue Sitzung erstellt. Für Anwendungen, die wissen sollen, dass sie in einer Terminalserversitzung laufen, gibt es eine Reihe von Windows-APIs, um diesen Umstand programmgesteuert zu ermitteln und um verschiedene Aspekte der Terminaldienste zu steuern. (Einzelheiten entnehmen Sie bitte dem Windows SDK und der Remote Desktop Service API.) Kapitel 2 beschreibt kurz, wie Sitzungen erstellt werden, und enthält einige Experimente, die zeigen, wie Sie sich Sitzungsinformationen mithilfe verschiedener Werkzeuge ansehen können, u. a. mit dem Kerneldebugger. Der Abschnitt »Objekt-Manager« in Kapitel 8 von Band 2 beschreibt, wie der Systemnamensraum für Objekte sitzungsweise instanziiert wird und wie Anwendungen, die über andere Instanzen auf demselben System Bescheid wissen müssen, diese Kenntnisse gewinnen können. In Kapitel 5 schließlich erfahren Sie, wie der Speicher-Manager sitzungsweite Daten einrichtet und verwaltet. Objekte und Handles Ein Kernelobjekt in Windows ist eine einzelne Laufzeitinstanz eines statisch definierten Objekttyps. Ein Objekttyp wiederum besteht aus einem im System definierten Datentyp, Funktionen für Instanzen dieses Datentyps sowie Objektattribute. Wenn Sie Windows-Anwendungen schreiben, werden Sie Prozess-, Thread-, Datei-, Ereignis- und sonstigen Objekten begegnen. All diese Objekte gehen auf maschinennähere Objekte zurück, die von Windows erstellt und verwaltet werden. Ein Prozess ist in Windows eine Instanz des Objekttyps für Prozesse, eine Datei eine Instanz des Objekttyps für Dateien usw. Ein Objektattribut ist ein Datenfeld in einem Objekt, das einen Teil des Objektzustands definiert. Beispielsweise hat ein Objekt vom Typ »Prozess« Attribute wie die Prozesskennung, eine grundlegende Planungspriorität und einen Zeiger auf ein Zugriffstokenobjekt. Objektmethoden sind die Vorkehrungen zur Bearbeitung von Objekten und lesen oder ändern gewöhnlich die Grundprinzipien und -begriffe Kapitel 1 33
Objektattribute. Beispielsweise nimmt die Methode open für einen Prozess eine Prozesskennung als Eingabe entgegen und gibt einen Zeitpunkt auf das Objekt als Ausgabe zurück. Es gibt auch den Parameter ObjectAttributes, den ein Aufrufer angibt, wenn er mit den Objekt-Manager-APIs des Kernels ein Objekt erstellt. Dieser Parameter darf nicht mit der breiteren Bedeutung des Wortes »Objektattribute« verwechselt werden. Der grundlegendste Unterschied zwischen einem Objekt und einer gewöhnlichen Datenstruktur besteht darin, dass die interne Struktur eines Objekts undurchsichtig ist. Um Daten aus einem Objekt zu gewinnen oder darin abzulegen, müssen Sie einen Objektdienst aufrufen. Es ist nicht möglich, die Daten in einem Objekt direkt zu lesen oder zu ändern. Dadurch wird die zugrunde liegende Implementierung des Objekts von dem Code, der es lediglich benutzt, getrennt, was es ermöglicht, Objektimplementierungen im Lauf der Zeit leicht zu ändern. Mithilfe des Objekt-Managers, einer Kernelkomponente, bieten Objekte eine bequeme Möglichkeit, um die folgenden wichtigen Betriebssystemaufgaben zu erfüllen: QQ Systemressourcen mit Namen versehen, die für Menschen lesbar sind QQ Ressourcen und Daten von Prozessen gemeinsam nutzen lasen QQ Ressourcen vor nicht autorisiertem Zugriff schützen QQ Verfolgen von Verweisen, wodurch das System es erkennt, wenn ein Objekt nicht mehr gebraucht wird, sodass seine Zuweisung automatisch aufgehoben werden kann Nicht alle Datenstrukturen in Windows sind Objekte. Nur Daten, die gemeinsam genutzt, geschützt, benannt oder (mithilfe von Systemdiensten) für Benutzermodusprogramme sichtbar gemacht werden müssen, werden in Objekte gestellt. Strukturen, die nur von einer Betriebssystemkomponente verwendet werden, um interne Funktionen zu implementieren, sind keine Objekte. Objekte und Handles (Verweise auf Objektinstanzen) werden ausführlicher in Kapitel 8 von Band 2 beschrieben. Sicherheit Windows wurde von Anfang an auf Sicherheit und auf die Erfüllung verschiedener formaler staatlicher und Branchensicherheitseinstufungen ausgelegt, darunter der CCITSE-Spezifikation (Common Criteria for Information Technology Security Evaluation). Eine staatlich anerkannte Sicherheitseinstufung macht ein Betriebssystem konkurrenzfähig. Natürlich sind viele der geforderten Eigenschaften für jedes Mehrbenutzersystem von Vorteil. Zu den wichtigsten Sicherheitseigenschaften von Windows gehören: QQ Bedingter (Kenntnis nur, wenn nötig) und verbindlicher Schutz für alle gemeinsam nutzbaren Systemobjekte wie Dateien, Verzeichnisse, Prozesse, Threads usw. QQ Sicherheitsüberwachung für die Verantwortlichkeit von Subjekten oder Benutzern und den von ihnen ausgelösten Aktionen 34 Kapitel 1 Begriffe und Werkzeuge
QQ Benutzerauthentifizierung bei der Anmeldung QQ Verhindern des Zugriffs eines Benutzers auf nicht initialisierte Ressourcen wie freien Arbeitsspeicher oder Festplattenplatz, deren Zuweisung ein anderer Benutzer aufgehoben hat Windows bietet drei Arten der Zugriffssteuerung für Objekte: QQ Bedingte Zugriffssteuerung Dies ist der Schutzmechanismus, an den die meisten Menschen beim Thema Betriebssystemsicherheit denken. Mit dieser Methode gewähren oder verweigern die Besitzer von Objekten (wie Dateien oder Druckern) den Zugriff für andere. Wenn sich ein Benutzer anmeldet, erhalt er einen Satz von Sicherheitsanmeldeinforma­ tionen oder einen Sicherheitskontext. Beim Versuch, auf ein Objekt zuzugreifen, wird dieser Sicherheitszugriff mit der Zugriffssteuerungsliste für das Objekt verglichen, um zu bestimmen, ob er über die erforderlichen Berechtigungen für die verlangte Operation verfügt. In Windows Server 2012 und Windows 8 wurde diese Art der bedingten Zugriffssteuerung durch die Einführung einer attributgestützten Zugriffssteuerung (oder »dynamischen Zugriffssteuerung«) noch verbessert. Dabei muss die Zugriffssteuerungsliste einer Ressource nicht mehr unbedingt einzelne Benutzer und Gruppen nennen, sondern kann auch die Attribute oder Claims angeben, die für den Zugriff auf die Ressource erforderlich sind, z. B. »Zugangsstufe: Top Secret« oder »Dienstalter: 10 Jahre«. Durch die Möglichkeit, SQL-Datenbanken und Schemata mit Aktive Directory durchsuchen und dadurch solche Attribute automatisch ausfüllen zu lassen, bietet diese Vorgehensweise ein erheblich eleganteres und flexibleres Sicherheitsmodell, bei dem sich Organisationen nicht mehr mühevoll um die manuelle Verwaltung von Gruppen und die Gruppenhierarchien kümmern müssen. QQ Privilegierte Zugriffssteuerung Sie ist dann erforderlich, wenn die bedingte Zugriffssteuerung nicht ausreicht. Mit dieser Methode wird dafür gesorgt, dass jemand an geschützte Objekte gelangen kann, wenn der Besitzer nicht verfügbar ist. Nehmen wir an, ein Angestellter scheidet aus einem Unternehmen aus. Der Administrator benötigt dann eine Möglichkeit, um Zugriff auf die Dateien zu bekommen, die nur für diesen Mitarbeiter zugänglich waren. In einem solchen Fall kann der Administrator unter Windows den Besitz der Datei übernehmen, um deren Rechte nach Bedarf einzurichten. QQ Verbindliche Zugriffssteuerung Diese Methode wird angewendet, wenn zusätzliche Sicherheitsvorkehrungen erforderlich sind, um Objekte zu schützen, auf die aus demselben Benutzerkonto heraus zugegriffen wird. Sie wird für verschiedene Zwecke eingesetzt, von einzelnen Aspekten der Sandboxtechnologie für Windows-Apps (siehe die nachfolgende Erörterung) über die Isolierung von Internet Explorer im geschützten Modus (und anderen Browsern) von der Benutzerkonfiguration bis zum Schutz von Objekten, die von einem Administratorkonto mit erhöhten Rechten erstellt wurden, vor dem Zugriff von Nicht-­ Administratorkonten ohne erhöhte Rechte. (Weitere Informationen über die Benutzerkontensteuerung erhalten Sie in Kapitel 7.) Ab Windows 8 wird eine als Anwendungscontainer (AppContainer) bezeichnete Sandbox für Windows-Apps verwendet. Sie bietet eine Trennung von anderen Anwendungscontainern und nicht zu Windows-Apps gehörenden Prozessen. Über wohldefinierte, von der WindowsLaufzeit­umgebung bereitgestellte Kontrakte kann Code in Anwendungscontainern mit Maklern (nicht isolierten Prozessen, die mit den Anmeldeinformationen des Benutzers ausgeführt wer- Grundprinzipien und -begriffe Kapitel 1 35
den) und manchmal auch mit anderen Anwendungscontainern oder Prozessen kommunizieren. Ein kanonisches Beispiel dafür ist der Browser Microsoft Edge; er läuft in einem Anwendungscontainer und bietet dadurch einen besseren Schutz gegen Schadcode, der in seinen Grenzen ausgeführt wird. Des Weiteren können Drittentwickler Anwendungscontainer auf ähnliche Weise zur Isolierung eigener Anwendungen nutzen, die keine Windows-Apps sind. Das Modell der Anwendungscontainer erzwingt einen erheblichen Wandel der herkömmlichen Programmierparadigmen, weg von der traditionellen Implementierung von Einzelprozess-Anwendungen mit mehreren Threads hin zu Anwendungen mit mehreren Prozessen. Sicherheit ist in der Windows-API allgegenwärtig. Das Windows-Teilsystem implementiert die objektgestützte Sicherheit auf die gleiche Weise wie das Betriebssystem: Es schützt gemeinsam genutzte Windows-Objekte vor nicht autorisiertem Zugriff, indem es sie mit Windows-Sicherheitsdeskriptoren versieht. Wenn eine Anwendung zum ersten Mal auf ein gemeinsam genutztes Objekt zugreift, überprüft das Windows-Teilsystem, ob sie das Recht dazu hat. Bei einem positiven Ausgang dieser Sicherheitsprüfung erlaubt das Windows-Teilsystem der Anwendung, fortzufahren. Eine ausführliche Beschreibung der Sicherheit in Windows erhalten Sie in Kapitel 7. Die Registrierung Bei der Arbeit mit Windows-Betriebssystemen haben Sie vielleicht schon einmal einen Blick in die Registrierung geworfen. Es ist schlecht möglich, die internen Mechanismen von Windows zu erläutern, ohne die Registrierung zu erwähnen, denn diese Systemdatenbank enthält Informa­ tionen über den Start und die Konfiguration des Systems, systemweite Softwareeinstellungen, die den Betrieb von Windows steuern, die Sicherheitsdatenbank und Konfigurationseinstellungen für die einzelnen Benutzer, beispielsweise den zu verwendenden Bildschirmschoner. Außerdem gewährt die Registrierung Einblicke in flüchtige Daten im Arbeitsspeicher, darunter den aktuellen Hardwarezustand des Systems (welche Gerätetreiber geladen sind, welche Ressourcen sie nutzen usw.) und in die Windows-Leistungsindikatoren. Diese Indikatoren befinden sich zwar nicht in der Registrierung, sind aber über Registrierungsfunktionen zugänglich (wobei es jedoch für den Zugriff auf sie eine neue, bessere API gibt). Wie Sie Informationen über Leistungsindikatoren über die Registrierung gewinnen, erfahren Sie in Kapitel 9 von Band 2. Windows-Benutzer und -Administratoren müssen nur selten einen Blick in die Registrierung werfen (da sich die meisten Konfigurationseinstellungen mit Standardverwaltungsprogrammen einsehen und ändern lassen), aber dennoch bildet sie eine nützliche Informationsquelle für Windows-Interna, da sie viele Einstellungen einschließt, die sich auf die Leistung und das Verhalten des Systems auswirken. Bei der Beschreibung einzelner Komponenten finden Sie in diesem Buch auch jeweils Hinweise auf die zugehörigen Registrierungsschlüssel. Die meisten der erwähnten Schlüssel befinden sich im Hive für die systemweite Konfigura­ tion – HKEY_LOCAL_MACHINE oder kurz HKLM. Wenn Sie Registrierungseinstellungen ändern, müssen Sie außerordentlich umsichtig vorgehen. Unsachgemäße Änderungen können die Systemleistung beeinträchtigen oder sogar dazu führen, dass das System nicht mehr hochgefahren werden kann. 36 Kapitel 1 Begriffe und Werkzeuge
Weitere Informationen über die Registrierung und ihren inneren Aufbau erhalten Sie in Kapitel 9 von Band 2. Unicode Windows unterscheidet sich von der Mehrzahl der anderen Betriebssysteme dadurch, dass seine meisten internen Textstrings als 16 Bit breite Unicode-Zeichen gespeichert und verarbeitet werden (genauer, im Format UTF-16LE; wenn in diesem Buch Unicode erwähnt wird, ist damit, sofern nicht anders angegeben, UTF-16LE gemeint). Unicode ist ein internationaler Zeichensatzstandard, der eindeutige Werte für die meisten Zeichensätze der Welt definiert und 8-, 16- und 32-Bit-Codierungen für jedes Zeichen bietet. Da viele Anwendungen ANSI-Zeichenstrings von 8 Bit (1 Byte) verarbeiten, nehmen viele Windows-Funktionen Stringparameter mit zwei Eintrittspunkten entgegen, einer Unicode-Version (breit, 16 Bit) und einer ANSI-Version (schmal, 8 Bit). Wenn Sie die schmale Version einer Windows-Funktion aufrufen, gibt es eine kleine Leistungseinbuße, da die Eingabestringparameter vor der Verarbeitung durch das System erst in Unicode und die Ausgabeparameter vor der Rückgabe an die Anwendung erst in ANSI umgewandelt werden. Bei älterem Code, der auf Windows laufen muss, aber noch mit ANSI-Zeichenstrings geschrieben ist, konvertiert Windows die ANSI-Zeichen für den eigenen Gebrauch in Unicode, aber niemals die Daten innerhalb von Dateien. Ob solche Daten als Unicode oder ANSI gespeichert werden, entscheidet die Anwendung. Unabhängig von der Sprache enthalten alle Versionen von Windows dieselben Funktionen. Anstelle von eigenständigen Sprachversionen hat Windows nur eine einzige, weltweit gültige Binärdatei, sodass eine einzelne Installation mehrere Sprachen unterstützen kann (durch Hinzufügen von Sprachpaketen). Es ist auch möglich, Windows-Funktionen zu nutzen, um eine Anwendung in Form einer einzigen, weltweiten Binärdatei bereitzustellen, die mehrere Sprachen unterstützt. Die alten Windows 9x-Betriebssysteme boten keine native Unterstützung für Unicode. Dies war ein weiterer Grund dafür, immer zwei Versionen einer Funktion jeweils für ANSI und für Unicode zu erstellen. So ist beispielsweise die Windows-API-Funktion CreateFile im Grunde genommen gar keine Funktion, sondern ein Makro, das entweder zu der Funktion CreateFileA (ANSI) oder CreateFileW (Unicode, wobei das W für »wide«, also »breit« steht) erweitert wird. Diese Erweiterung erfolgt aufgrund der Kompilierungskonstanten UNICODE, die in Visual Studio C++-Projekten standardmäßig gesetzt ist, da es vorteilhafter ist, mit Unicode-Funktionen zu arbeiten. Es ist jedoch möglich, statt des Makros auch ausdrücklich den Funktionsnamen zu verwenden. Das folgende Experiment führt diese Funktionspaare vor. Grundprinzipien und -begriffe Kapitel 1 37
Experiment: Exportierte Funktionen einsehen In diesem Experiment sehen wir uns die aus einer Windows-Teilsystem-DLL exportierten Funktionen mit dem Tool Dependency Walker an. 1. Laden Sie Dependency Walker von http://dependencywalker.com herunter, je nach Ihrem System die 32- oder die 64-Bit-Version. Entpacken Sie die heruntergeladene ZIP-Datei in einem Ordner Ihrer Wahl. 2. Führen Sie das Tool aus (depends.exe). Öffnen Sie das Menü File, wählen Sie Open, wechseln Sie zum Ordner C:\Windows\System32 (sofern Windows bei Ihnen auf C in­ stalliert ist), markieren Sie die Datei kernel32.dll und klicken Sie auf Öffnen. 3. Möglicherweise zeigt Dependency Walker eine Warnung an. Sie können sie getrost ignorieren und schließen. 4. Es werden jetzt mehrere Ansichten mit vertikalen und horizontalen Trennleisten angezeigt. Stellen Sie sicher, dass in der Baumansicht oben links kernel32.dll markiert ist. 5. Schauen Sie sich die zweite Ansicht von oben auf der rechten Seite an. Dort sind die exportierten Funktionen aus kernel32.dll aufgeführt. Klicken Sie auf den Spaltenkopf Function, um die Liste nach Namen zu sortieren. Suchen Sie die Funktion CreateFileA. Nicht weit darunter finden Sie auch CreateFileW: 6. Wie Sie sehen, handelt es sich bei den meisten Funktionen, die mindestens ein Stringtype-Argument haben, in Wirklichkeit um Funktionspaare. In der vorstehenden Abbildung sehen Sie CreateFileMappingA/W, CreateFileTransactedA/W und CreateFileMappingNumaA/W. 7. Scrollen Sie durch die Liste, um weitere Funktionspaare zu finden. Sie können auch andere Systemdateien öffnen, z. B. user32.dll und advapi32.dll. 38 Kapitel 1 Begriffe und Werkzeuge
Die COM-gestützten APIs in Windows verwenden gewöhnlich Unicode-Strings, manchmal vom Typ BSTR. Dabei handelt es sich im Grunde genommen um ein mit NULL abgeschlossenes Array aus Unicode-Zeichen, bei dem die Stringlänge in vier Bytes vor dem Beginn des Arrays im Arbeitsspeicher gespeichert ist. Windows Runtime-APIs nutzen nur Unicode-Strings vom Typ HSTRING, bei denen es sich um unveränderliche Unicode-Zeichenarrays handelt. Weitere Informationen über Unicode erhalten Sie auf http://www.unicode.org und in der Programmierdokumentation der MSDN Library. Die internen Mechanismen von Windows untersuchen Viele der Informationen, die wir in diesem Buch geben, gehen auf die Lektüre von Windows-Quellcode und Gespräche mit Entwicklern zurück. Sie müssen uns aber nicht alles ungeprüft glauben. Viele Einzelheiten der internen Mechanismen von Windows lassen sich durch eine Reihe von Werkzeugen sichtbar machen und vorführen, unter anderem auch mit den Tools, die im Lieferumfang von Windows und den Windows-Debuggingtools enthalten sind. Diese Werkzeugpakete werden wir weiter hinten in diesem Abschnitt noch kurz behandeln. Um Sie dazu zu ermutigen, die internen Mechanismen von Windows selbst zu erkunden, geben wir überall in diesem Buch Anleitungen zu »Experimenten«, mit denen Sie sich jeweils einen bestimmten Aspekt des internen Verhaltens von Windows ansehen können. Einige dieser Experimente haben Sie in diesem Kapitel schon gesehen. Führen Sie diese Experimente aus, um die in diesem Buch beschriebenen Vorgänge in Aktion zu erleben. Tabelle 1–4 nennt die wichtigsten in diesem Buch verwendeten Werkzeuge und ihren Ursprung. Werkzeug Image Herkunft Startup Programs Viewer AUTORUNS Sysinternals Access Check ACCESSCHK Sysinternals Dependency Walker DEPENDS www.dependencywalker.com Global Flags GFLAGS Debugging Tools Handle Viewer HANDLE Sysinternals Kernel Debuggers WINDBG, KD WDK, Windows SDK Object Viewer WINOBJ Sysinternals Leistungsüberwachung PERFMON.MSC In Windows enthalten Pool Monitor POOLMON WDK Process Explorer PROCEXP Sysinternals Process Monitor PROCMON Sysinternals Task (Process) List TLIST Debugging tools Task-Manager TASKMGR In Windows enthalten Tabelle 1–4 Werkzeuge zur Untersuchung der internen Mechanismen von Windows Die internen Mechanismen von Windows untersuchen Kapitel 1 39
Leistungsüberwachung und Ressourcenmonitor In diesem Buch werden wir häufig die Leistungsüberwachung verwenden, die Sie über den Ordner Verwaltung in der Systemsteuerung oder durch die Eingabe von perfmon in das Dia­ logfeld Ausführen erreichen. Insbesondere konzentrieren wir uns dabei auf die Werkzeuge Leistungsüberwachung und Ressourcenmonitor. Die Leistungsüberwachung hat drei Funktionen: Systemüberwachung, Anzeige von Leistungsindikatorenprotokollen und Einrichten von Warnungen (mithilfe von Datensammlersätzen, die wiederum Leistungsindikatorenprotokolle sowie Ablaufverfolgungs- sowie Konfigurationsdaten nutzen). Der Einfachheit halber schreiben wir Leistungsüberwachung, wenn wir die darin enthaltene Funktion zur Systemüberwachung meinen. Die Leistungsüberwachung bietet mehr Informationen über das Funktionieren des Systems als jedes andere einzelne Werkzeug. Sie finden Hunderte von grundlegenden und erweiterbaren Indikatoren für verschiedene Objekte. Bei jedem Thema, das wir in diesem Buch beschreiben, geben wir auch jeweils eine Tabelle der entsprechenden Windows-Leistungsindikatoren an. In der Leistungsüberwachung finden Sie eine kurze Beschreibung der einzelnen Indikatoren. Um sie einzusehen, markieren Sie im Fenster Leistungsindikatoren hinzufügen einen Indikator und aktivieren das Kontrollkästchen Beschreibung anzeigen. Obwohl sich die gesamte maschinennahe Überwachung, die wir in diesem Buch durchführen, mit der Leistungsüberwachung erledigen lässt, bietet Windows noch ein weiteres Instrument, nämlich den Ressourcenmonitor (den Sie über das Startmenü oder die Registerkarte Leistung des Task-Managers erreichen). Er zeigt die vier wichtigsten Systemressourcen an, nämlich CPU, Festplatte, Netzwerk und Arbeitsspeicher. In der Grundansicht werden dazu die gleichen Informationen angezeigt wie im Task-Manager. Die einzelnen Abschnitte können jedoch auch erweitert werden, um zusätzliche Angaben einzusehen. Die folgende Abbildung zeigt eine typische Ansicht des Ressourcenmonitors: 40 Kapitel 1 Begriffe und Werkzeuge
Im erweiterten Zustand zeigt die Registerkarte CPU Informationen über die CPU-Nutzung durch die einzelnen Prozesse an. Das können Sie auch im Task-Manager sehen, aber hier gibt es noch eine zusätzliche Spalte für die durchschnittliche CPU-Nutzung, die Ihnen eine bessere Vorstellung davon vermitteln kann, welche Prozesse am aktivsten sind. Die Registerkarte CPU enthält auch eine separate Anzeige der Dienste, ihrer CPU-Nutzung und ihrer durchschnittlichen Nutzung. Bei jedem Diensthostingprozess ist die zugehörige Dienstgruppe angegeben. Wie in Process Explorer können Sie einen Prozess auswählen (indem Sie auf das zugehörige Kontrollkästchen klicken), wodurch eine Liste der benannten Handles angezeigt wird, die der Prozess geöffnet hat, sowie eine Liste der Module (z. B. DLLs), die in dem Adressraum des Prozesses geladen wurden. Im Feld Handles durchsuchen können Sie auch nach den Prozessen suchen, die für eine gegebene benannte Ressource ein Handle geöffnet haben. Die Registerkarte Arbeitsspeicher zeigt fast die gleichen Informationen an wie die entsprechende Registerkarte im Task-Manager, ist aber für das gesamte System ausgelegt. Ein Balkendiagramm zeigt die aktuelle Aufteilung des physischen Arbeitsspeichers in die Abschnitte Für Hardware reserviert, In Verwendung, Geändert, Standby und Frei. Was diese Begriffe bedeuten, erfahren Sie in Kapitel 5. Auf der Registerkarte Datenträger sehen Sie Angaben zur E/A der einzelnen Dateien in einer Darstellung, in der Sie leicht die Dateien auf dem System erkennen, auf die am meisten zugriffen, in die am meisten geschrieben oder in denen am meisten gelesen wird. Die Ergebnisse lassen sich darüber hinaus noch nach Prozessen filtern. Die Registerkarte Netzwerk gibt die aktiven Netzwerkverbindungen, ihre Besitzerprozesse und die Menge der über sie übertragenen Daten an. Dadurch können Sie Netzwerkaktivitäten erkennen, die im Hintergrund ablaufen und sich sonst nur schwer aufspüren lassen. Außerdem Die internen Mechanismen von Windows untersuchen Kapitel 1 41
werden die aktiven TCP-Verbindungen angezeigt, geordnet nach Prozessen und mit Daten wie Remoteport, lokaler Port, Remoteadresse, lokale Adresse, Paketverluste und Latenz. Des Weiteren befindet sich hier eine nach Prozessen geordnete Liste der Ports, die es Administratoren ermöglicht, zu erkennen, welche Dienste oder Anwendungen gerade an einem gegebenen Port auf eine Verbindung warten. Für die einzelnen Ports und Prozesse sind auch jeweils das Protokoll und die Firewallrichtlinie angegeben. Alle Windows-Leistungsindikatoren sind in Programmen zugänglich. Um weitere Informationen darüber zu erhalten, suchen Sie in der MSDN-Dokumentation nach »performance counters«. Kernel-Debugging Kernel-Debugging bedeutet, die internen Datenstrukturen des Kernels zu untersuchen und die Funktionen im Kernel schrittweise zu durchlaufen. Das ist eine praktische Vorgehensweise, um die internen Mechanismen von Windows zu untersuchen, da Sie damit interne Systeminformationen einsehen können, die über andere Werkzeugen nicht verfügbar sind, und eine bessere Vorstellung des Codeflusses innerhalb des Kernels erhalten. Bevor wir die verschiedenen Möglichkeiten für das Kernel-Debugging beschreiben, wollen wir uns einige Dateien ansehen, die Sie dafür brauchen. Symbole für das Kernel-Debugging Symboldateien enthalten die Namen von Funktionen und Variablen und das Layout und Format von Datenstrukturen. Sie werden vom Linker erstellt und von Debuggern genutzt, um beim Debugging auf diese Namen zu verweisen und sie anzuzeigen. Diese Informationen sind gewöhnlich nicht im Binärabbild gespeichert, da sie zur Ausführung des Codes nicht erforderlich sind. Dadurch können die Binärdateien kleiner und schneller gemacht werden. Allerdings müssen Sie beim Debugging dafür sorgen, dass der Debugger auf die Symboldateien für die Images zugreifen kann, mit denen Sie arbeiten wollen. Um irgendeines der Kernel-Debugging-Tools verwenden zu können, um interne Daten­ strukturen des Windows-Kernels wie die Prozessliste, Threadblockierungen, die Liste der geladenen Treiber, Angaben über die Speichernutzung usw. zu untersuchen, brauchen Sie die Symboldateien mindestens für das Kernelabbild Ntoskrnl.exe. (Mehr über diese Datei erfahren Sie im Abschnitt »Die Architektur im Überblick« in Kapitel 2.) Die Symboltabellendateien müssen dabei jeweils der Version des Images entsprechen, das Sie untersuchen. Wenn Sie beispielsweise den Kernel mit einem Windows-Servicepack oder einem Hotfix aktualisiert haben, müssen Sie auch entsprechend aktualisierte Symboldateien verwenden. Es ist zwar möglich, die Symbole für verschiedene Windows-Versionen herunterzuladen und zu installieren, doch aktualisierte Versionen für Hotfixes sind nicht immer verfügbar. Die einfachste Möglichkeit, um die korrekte Version der Symbole für das Debugging zu bekommen, besteht darin, einen Microsoft-Symbolserver zu nutzen, indem Sie im Debugger eine besondere Syntax für den Symbolpfad angeben. Beispielsweise sorgt der folgende Symbolpfad 42 Kapitel 1 Begriffe und Werkzeuge
dafür, dass der Debugger die erforderlichen Symbole von dem Symbolserver im Internet herunterlädt und eine lokale Kopie im Ordner C:\symbols ablegt: srv*c:\symbols*http://msdl.microsoft.com/download/symbols Debugging Tools for Windows Das Paket Debugging Tools for Windows enthält ausgefeilte Debugging-Werkzeuge, die wir in diesem Buch zur Untersuchung der inneren Mechanismen von Windows einsetzen. Die neueste Version ist im Windows SDK enthalten. (Weitere Informationen über die verschiedenen Installationsarten finden Sie auf https://msdn.microsoft.com/en-us/library/windows/hardware/ ff551063.aspx.) Die Werkzeuge eignen sich zum Debugging von Prozessen sowohl im Benutzer- als auch im Kernelmodus. In dem Paket sind die vier Debugger cdb, ntsd, kd und WindDbg enthalten. Sie alle basieren auf derselben Debugging-Engine, die in DbgEng.dll implementiert und in der Hilfedatei für das Paket ziemlich gut dokumentiert ist. Die Debugger weisen folgende Eigenschaften auf: QQ cdb und ntsd sind Debugger für den Benutzermodus, die in einer Konsole ausgeführt werden. Der einzige Unterschied zwischen ihnen besteht darin, dass ntsd ein neues Konsolenfenster öffnet, wenn er von einem bereits vorhandenen Konsolenfenster aus aufgerufen wird, was bei cdb nicht der Fall ist. QQ kd ist ein Debugger für den Kernelmodus, der in einer Konsole ausgeführt wird. QQ WinDbg kann sowohl für den Benutzer- als auch den Kernelmodus verwendet werden, aber nicht gleichzeitig. Er verfügt über eine grafische Benutzeroberfläche. QQ Die Benutzermodus-Debugger (cdb, ntsd und WinDbg) sind im Grunde genommen gleichwertig. Welchen Sie davon bevorzugen, ist Geschmackssache. QQ Auch die Kernelmodus-Debugger (kd und WinDbg) sind gleichwertig. Debugging im Benutzermodus Mit den Debugging-Tools können Sie sich auch in einen Benutzermodus-Prozess einklinken und den Prozessarbeitsspeicher untersuchen und ändern. Dabei gibt es zwei Vorgehensweisen: QQ Invasiv Wenn Sie sich in einen laufenden Prozess einklinken, verwenden Sie standardmäßig die Windows-Funktion DebugActiveProcess, um eine Verbindung zwischen dem Debugger und dem zu untersuchenden Prozess herzustellen. Dadurch können Sie den Prozessspeicher untersuchen und ändern, Haltepunkte setzen und andere Debuggingfunktionen ausführen. Wenn Sie den Debugger nur trennen und nicht ausschalten, können Sie den Debuggingvorgang abbrechen, ohne den Zielprozess zu beenden. QQ Nicht invasiv Bei dieser Option öffnet der Debugger den Prozess mit der Funktion OpenProcess, ohne sich in ihn einzuklinken. Dadurch können Sie den Arbeitsspeicher des Zielprozesses untersuchen und ändern, aber keine Haltepunkte setzen. Die nicht invasive Vorgehensweise ist auch dann möglich, wenn sich ein anderer Debugger invasiv eingeklinkt hat. Die internen Mechanismen von Windows untersuchen Kapitel 1 43
Mit den Debugging-Tools können Sie auch Dumpdateien von Benutzermodus-Prozessen öffnen. Diese Dateien werden in Kapitel 8 von Band 2 im Abschnitt über Ausnahmezuteilung erklärt. Debugging im Kernelmodus Wie bereits erwähnt, sind zwei der Tools auch für das Kerneldebugging geeignet, nämlich das Befehlszeilenwerkzeug Kd.exe und das GUI-Programm WinDbg.exe. Damit können Sie drei verschiedene Arten von Kerneldebugging durchführen: QQ Öffnen einer Absturzabbilddatei, die infolge eines Windows-Systemabsturzes erstellt wurde. (Mehr über Kernelabsturzabbilder erfahren Sie in Kapitel 15, »Analyse von Absturzabbildern«, von Band 2.) QQ Verbindung zu einem laufenden System und Untersuchung von dessen Status (bzw. Setzen von Haltepunkten, wenn Sie Gerätetreibercode debuggen). Für diesen Vorgang brauchen Sie zwei Computer, nämlich das Ziel (das zu debuggende System) und den Host (das System, auf dem der Debugger ausgeführt wird). Die Verbindung kann über ein Nullmodemkabel, ein IEEE-1394-Kabel, ein USB-2.0/3.0-Debuggingkabel oder das lokale Netzwerk erfolgen. Das Zielsystem muss im Debugmodus gestartet werden. Dafür können Sie Bcdedit.exe oder msconfig.exe entsprechend einrichten. (Möglicherweise müssen Sie dabei auf den sicheren Start in den UEFI-BIOS-Einstellungen verzichten.) Wenn Sie Windows 7 oder frühere Versionen über ein Virtualisierungsprodukt wie Hyper-V, VirtualBox oder VMware Workstation debuggen, können Sie auch den seriellen Port des Gastbetriebssystems als benannte Pipe verfügbar machen und darüber eine Verbindung herstellen. Für Gastbetriebssysteme ab Windows 8 sollten Sie jedoch das Debugging über das lokale Netzwerk durchführen, indem Sie das Hostnetzwerk über eine virtuelle Netzwerkkarte im Gastbetriebssystem bereitstellen. Das führt zu einem Leistungsgewinn um den Faktor 1000. QQ Bei Windows-Systemen können Sie auch Verbindung zum lokalen System aufnehmen und dessen Zustand untersuchen. Diese Vorgehensweise wird lokales Kerneldebugging genannt. Für das Debugging mit WinDbg müssen Sie das System erst in den Debugmodus versetzen (etwa indem Sie Msconfig.exe ausführen, die Registerkarte Start aufrufen, dort auf Erweiterte Optionen klicken, Debug aktivieren und Windows neu starten). Starten Sie anschließend WinDbg mit Administratorrechten, öffnen Sie das Menü File, wählen Sie Kernel Debug, klicken Sie auf die Registerkarte Local und dann auf OK (oder verwenden Sie Bcdedit.exe). Abb. 1–6 zeigt eine Beispielausgabe auf einem Computer mit der 64-Bit-Version von Windows 10. Im lokalen Kerneldebuggingmodus funktionieren einige der Befehle für das Kerneldebugging nicht. Unter anderem können Sie keine Haltepunkte setzen und mit dem Befehl .dump keinen Speicherauszug erstellen. Letzteres ist allerdings mit LiveKd möglich, das weiter hinten in diesem Abschnitt beschrieben wird. 44 Kapitel 1 Begriffe und Werkzeuge
Abbildung 1–6 Lokales Kerneldebugging Sobald die Verbindung im Kerneldebuggingmodus hergestellt ist, können Sie eine der vielen Debuggererweiterungen nutzen – der sogenannten Bang-Befehle, weil sie mit einem Ausrufezeichen beginnen –, um den Inhalt von internen Datenstrukturen wie Threads, Prozessen und E/A-Anforderungspaketen sowie Speicherverwaltungsinformationen anzuzeigen. In diesem Buch sind immer die zum jeweiligen Thema passenden Kerneldebuggerbefehle und deren Ausgaben angegeben. Eine hervorragende Möglichkeit zum Nachschlagen bietet die Hilfedatei Debugger.chm, die im Installationsordner von WinDbg enthalten ist und den gesamten Funktionsumfang des Kerneldebuggers und seiner Erweiterungen dokumentiert. Außerdem können Sie mit dem Befehl dt (display type) für mehr als 1000 Kernelstrukturen Typinformationen anzeigen lassen, die in der Kernelsymboldatei für Windows enthalten sind. Experiment: Typinformationen über Kernelstrukturen Um sich die Liste der Kernelstrukturen anzusehen, deren Typinformationen in den Kernelsymbolen enthalten sind, geben Sie im Kerneldebugger dt nt!_* ein. Dadurch erhalten Sie die folgende (hier gekürzt wiedergegebene) Ausgabe: lkd> dt nt!_* ntkrnlmp!_KSYSTEM_TIME ntkrnlmp!_NT_PRODUCT_TYPE ntkrnlmp!_ALTERNATIVE_ARCHITECTURE_TYPE ntkrnlmp!_KUSER_SHARED_DATA ntkrnlmp!_ULARGE_INTEGER ntkrnlmp!_TP_POOL ntkrnlmp!_TP_CLEANUP_GROUP ntkrnlmp!_ACTIVATION_CONTEXT ntkrnlmp!_TP_CALLBACK_INSTANCE → Die internen Mechanismen von Windows untersuchen Kapitel 1 45
ntkrnlmp!_TP_CALLBACK_PRIORITY ntkrnlmp!_TP_CALLBACK_ENVIRON_V3 ntkrnlmp!_TEB Dabei ist ntkrnlmp der interne Dateiname des 64-Bit-Kernels (siehe Kapitel 2). Mit dem Befehl dt können Sie auch mit Jokerzeichen nach bestimmten Strukturen suchen. Wenn Sie beispielsweise den Namen der Struktur für ein Interruptobjekt benötigen, können Sie dt nt!_*interrupt* eingeben: lkd> dt nt!_*interrupt* ntkrnlmp!_KINTERRUPT_MODE ntkrnlmp!_KINTERRUPT_POLARITY ntkrnlmp!_PEP_ACPI_INTERRUPT_RESOURCE ntkrnlmp!_KINTERRUPT ntkrnlmp!_UNEXPECTED_INTERRUPT ntkrnlmp!_INTERRUPT_CONNECTION_DATA ntkrnlmp!_INTERRUPT_VECTOR_DATA ntkrnlmp!_INTERRUPT_HT_INTR_INFO ntkrnlmp!_INTERRUPT_REMAPPING_INFO Wenn Sie bei dt wie folgt den Namen einer einzelnen Struktur angeben (wobei nicht zwischen Groß- und Kleinschreibung unterschieden wird), können Sie genauere Informationen darüber einsehen: lkd> dt nt!_KINTERRUPT +0x000 Type : Int2B +0x002 Size : Int2B +0x008 InterruptListEntry : _LIST_ENTRY +0x018 ServiceRoutine : Ptr64 unsigned char +0x020 MessageServiceRoutine : Ptr64 unsigned char +0x028 MessageIndex : Uint4B +0x030 ServiceContext : Ptr64 Void +0x038 SpinLock : Uint8B +0x040 TickCount : Uint4B +0x048 ActualLock : Ptr64 Uint8B +0x050 DispatchAddress : Ptr64 void +0x058 Vector : Uint4B +0x05c Irql : UChar +0x05d SynchronizeIrql : UChar +0x05e FloatingSave : UChar +0x05f Connected : UChar +0x060 Number : Uint4B +0x064 ShareVector : UChar +0x065 EmulateActiveBoth : UChar → 46 Kapitel 1 Begriffe und Werkzeuge
+0x066 ActiveCount : Uint2B +0x068 InternalState : Int4B +0x06c Mode : _KINTERRUPT_MODE +0x070 Polarity : _KINTERRUPT_POLARITY +0x074 ServiceCount : Uint4B +0x078 DispatchCount : Uint4B +0x080 PassiveEvent : Ptr64 _KEVENT +0x088 TrapFrame : Ptr64 _KTRAP_FRAME +0x090 DisconnectData : Ptr64 Void +0x098 ServiceThread : Ptr64 _KTHREAD +0x0a0 ConnectionData : Ptr64 _INTERRUPT_CONNECTION_DATA +0x0a8 IntTrackEntry : Ptr64 Void +0x0b0 IsrDpcStats : _ISRDPCSTATS +0x0f0 RedirectObject : Ptr64 Void +0x0f8 Padding : [8] UChar Standardmäßig zeigt dt keine Unterstrukturen an. Dazu müssen Sie den Schalter -r oder -b verwenden. (Den genauen Unterschied zwischen diesen beiden Schaltern entnehmen Sie bitte der Dokumentation.) Beispielsweise können Sie diese Schalter bei der Anzeige des Kernelinterruptobjekts angeben, um sich das Format der Struktur _LIST_ENTRY anzeigen zu lassen, die im Feld InterruptListEntry gespeichert ist: lkd> dt nt!_KINTERRUPT -r +0x000 Type : Int2B +0x002 Size : Int2B +0x008 InterruptListEntry : _LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x000 Flink +0x008 Blink +0x008 Blink : Ptr64 _LIST_ENTRY : Ptr64 _LIST_ENTRY : Ptr64 _LIST_ENTRY +0x000 Flink +0x008 Blink +0x018 ServiceRoutine : Ptr64 _LIST_ENTRY : Ptr64 _LIST_ENTRY : Ptr64 unsigned char Bei dt können Sie sogar die Verschachtelungstiefe der anzuzeigenden Strukturen festlegen, indem Sie hinter dem Schalter -r eine Zahl angeben. Mit dem Befehl im folgenden Beispiel wählen Sie eine Verschachtelungstiefe von einer Ebene aus: lkd> dt nt!_KINTERRUPT -r1 Die Hilfedatei zu den Debugging Tools for Windows erklärt Ihnen, wie Sie die Kerneldebugger einrichten und verwenden. Weitere Einzelheiten darüber, die vor allem für die Autoren von Gerätetreibern interessant sind, finden Sie in der WDK-Dokumentation. Die internen Mechanismen von Windows untersuchen Kapitel 1 47
LiveKd LiveKd ist ein kostenloses Tool von Sysinternals, mit dem es möglich ist, das laufende System mit den zuvor beschriebenen Kerneldebuggern von Microsoft zu untersuchen, ohne es im Debugmodus zu starten. Das kann erforderlich sein, wenn Sie eine Störungssuche auf Kernelebene an einem Computer durchführen müssen, der nicht im Debugmodus läuft. Da sich manche Probleme nur schwer zuverlässig reproduzieren lassen, kann es sein, dass sich der Fehler nach einem Neustart mit der Debugoption nicht mehr so deutlich zeigt. LiveKd führen Sie genauso aus wie WinDbg und kd. Es übergibt alle von Ihnen eingegebenen Befehlszeilenoptionen an den Debugger Ihrer Wahl. Standardmäßig führt LiveKd die Befehlszeilenversion des Kerneldebuggers aus (kd), doch mit der Option -w können Sie auf WinDbg umschalten. Um die Hilfedateien für die Befehlszeilenoptionen von LiveKd einzusehen, verwenden Sie den Schalter -?. LiveKd stellt dem Debugger eine simulierte Absturzabbilddatei bereit. Daher können Sie in LiveKd jegliche Operationen durchführen, die auch bei einem Absturzabbild möglich sind. Da LiveKd für dieses Abbild auf den physischen Arbeitsspeicher zurückgreift, kann der Debugger auf Datenstrukturen stoßen, die gerade geändert werden und daher inkonsistent sind. Jedes Mal, wenn der Debugger neu gestartet wird, sieht er eine aktuelle Ansicht des Systemzustands. Sie können diese Momentaufnahme auch aktualisieren, indem Sie den Befehl q eingeben, um den Debugger zu beenden. LiveKd fragt Sie dann, ob Sie ihn neu starten wollen. Wenn der Debugger bei der Ausgabe in eine Schleife gerät, können Sie sie mit (Strg) + (C) abbrechen und beenden. Hängt der Debugger, drücken Sie (Strg) + (Untbr), wodurch der Debuggerprozess beendet wird. LiveKd fragt Sie hier ebenfalls, ob Sie ihn neu starten wollen. Windows Software Development Kit Das Windows Software Development Kit (SDK) ist im Rahmen des MSDN-Abonnementprogramms erhältlich. Sie können es kostenlos von https://developer.microsoft.com/en-US/ windows/downloads/windows-10-sdk herunterladen. Auch bei der Installation von Visual Studio haben Sie die Möglichkeit, das SDK zu installieren. Im Abonnement erhalten Sie immer die neueste Version, wohingegen in Visual Studio auch eine ältere Version enthalten sein kann, die beim Kauf von VS gerade aktuell war. Neben den Debugging Tools for Windows enthält das SDK auch die erforderlichen C-Headerdateien und Bibliotheken zum Kompilieren und Verlinken von Windows-Anwendungen. Für unsere Zwecke sind vor allem die Windows-API-Headerdateien (z. B. C:\Program Files (x86)\Windows Kits\10\Include) und die SDK-Tools (im Ordner Bin) von Bedeutung. Ebenso von Interesse ist die Dokumentation, die online zur Verfügung steht und für den Offlinezugriff heruntergeladen werden kann. Einige der Werkzeuge sind auch als Beispielquellcode sowohl im Windows SDK als auch in MSDN Library erhältlich. 48 Kapitel 1 Begriffe und Werkzeuge
Windows Driver Kit Das Windows Driver Kit (WDK) ist ebenfalls im MSDN-Abonnementprogramm verfügbar und kann kostenlos heruntergeladen werden. Die WDK-Dokumentation finden Sie in MSDN Library. Das WDK ist zwar vor allem für Entwickler von Gerätetreibern gedacht, bietet aber auch eine Fülle von Informationen über interne Windows-Mechanismen. Um Ihnen ein Beispiel zu geben: Kapitel 6 erklärt zwar die Architektur des E/A-Systems, das Treibermodell und die grundlegenden Datenstrukturen von Gerätetreibern, aber nicht die einzelnen Kernelunterstützungsfunktionen. Eine ausführliche Beschreibung dieser Funktionen und der von den Gerätetreibern verwendeten Mechanismen finden Sie jedoch in der WDK-Dokumentation, sowohl in Form von Tutorials als auch eines Nachschlagewerks. Außer dieser Dokumentation enthält das WDK Headerdateien (insbesondere ntddk.h, ntifs.h und wdm.h), die entscheidende interne Datenstrukturen und Konstanten sowie Schnittstellen zu vielen internen Systemroutinen definieren. Diese Dateien sind praktisch, wenn Sie die internen Datenstrukturen von Windows mit einem Kerneldebugger untersuchen, da in diesem Buch zwar das allgemeine Layout und der Inhalt dieser Strukturen angegeben wird, aber keine Beschreibungen der einzelnen Felder (z. B. Angaben über Größe und Datentyp). Zahlreiche dieser Datenstrukturen – etwa Objektdispatcherheader, Warteblöcke, Ereignisse, Mutanten, Semaphoren usw. – sind jedoch ausführlich im WDK beschrieben. Wenn Sie über die Darstellung in diesem Buch noch tiefer in das E/A-System und das Treibermodell einsteigen möchten, sollten Sie die WDK-Dokumentation lesen, insbesondere die Handbücher Kernel-Mode Driver Architecture Design Guide und Kernel-Mode Driver Reference. Ebenfalls nützlich sind die Bücher Programming the Microsoft Windows Driver Model, Second Edition von Walter Oney (Microsoft Press, 2002) und Developing Drivers with the Windows Driver Foundation von Penny Orwick und Guy Smith (Microsoft Press, 2007). Sysinternals-Werkzeuge Für viele Experimente in diesem Buch werden Freeware-Tools verwendet, die Sie von Sysinternals herunterladen können. Die meisten davon hat Mark Russinovich geschrieben, einer der Autoren dieses Buches. Die am häufigsten genutzten dieser Werkzeuge sind Process Explorer und Process Monitor. Beachten Sie, dass Sie für viele dieser Tools Kernelmodus-Gerätetreiber installieren und ausführen müssen, wofür Administrator- oder erhöhte Rechte erforderlich sind. Einige der Werkzeuge können jedoch auch mit eingeschränktem Funktionsumfang und eingeschränkter Ausgabe von Standardbenutzerkonten ohne erhöhte Rechte ausgeführt werden. Die Sysinternals-Tools werden regelmäßig aktualisiert. Achten Sie daher darauf, dass Sie die neueste Version verwenden. Um sich über Aktualisierungen zu informieren, folgen Sie dem Blog auf der Sysinternals-Website (für die es auch einen RSS-Feed gibt). Eine Beschreibung all dieser Werkzeuge und ihrer Verwendung sowie Fallstudien über damit gelöste Probleme finden Sie in Windows Sysinternals Administrator’s Reference von Mark Russinovich und Aaron Margosis (Microsoft Press, 2011). Wenn Sie Fragen über die Tools stellen oder darüber diskutieren möchten, nutzen Sie das Sysinternals-Forum. Die internen Mechanismen von Windows untersuchen Kapitel 1 49
Zusammenfassung In diesem Kapitel haben Sie eine Einführung in wichtige technische Prinzipien und Grundbegriffe von Windows erhalten, die im weiteren Verlauf dieses Buches verwendet werden. Des Weiteren haben Sie einen Eindruck von den vielen praktischen Tools bekommen, mit denen Sie die inneren Mechanismen von Windows untersuchen können. Damit sind Sie jetzt gewappnet, die interne Gestaltung des Systems zu erkunden. Dabei beginnen wir mit einem Überblick über die Systemarchitektur und ihre Hauptkomponenten. 50 Kapitel 1 Begriffe und Werkzeuge
K apite l 2 Systemarchitektur Nachdem Sie die erforderlichen Grundbegriffe, Prinzipien und Werkzeuge kennengelernt haben, können Sie nun damit beginnen, die Designziele und internen Strukturen des Betriebssystems Microsoft Windows zu erkunden. In diesem Kapitel geht es um die Gesamtarchitektur des Systems – die Hauptkomponenten, ihre Wechselbeziehungen zueinander und den Kontext, in dem sie ausgeführt werden. Um Ihnen ein Gerüst für das Verständnis der inneren Mechanismen von Windows zu geben, wollen wir uns zunächst die Anforderungen und Ziele ansehen, die das ursprüngliche Design und die Spezifikation des Systems bestimmt haben. Anforderungen und Designziele Die folgenden Anforderungen bestimmten die Spezifikation von Windows NT im Jahr 1989: QQ Es sollte ein präemptives, eintrittsvariantes echtes 32-Bit-Betriebssystem mit virtuellem Speicher entwickelt werden. QQ Das Betriebssystem sollte auf mehreren Hardwarearchitekturen und -plattformen laufen. QQ Es sollte auf Systemen mit symmetrischem Multiprocessing laufen und gut skalierbar sein. QQ Es sollte eine hervorragende Plattform für verteilte Verarbeitung bilden, sowohl als Netzwerkclient als auch als Server. QQ Es sollte die meisten vorhandenen 16-Bit-MS-DOS- und Microsoft Windows 3.1-Anwendungen ausführen. QQ Es sollte die staatlichen Anforderungen für POSIX-1003.1-Konformität erfüllen. QQ Es sollte die staatlichen und die Branchenanforderungen für Betriebssystemsicherheit erfüllen. QQ Es sollte Unicode unterstützen, um leicht an den weltweiten Markt angepasst werden zu können. Um ein System zu erschaffen, das all diese Anforderungen erfüllte, mussten Tausende von Entscheidungen getroffen werden. Um diesen Vorgang zu steuern, stellte das Designteam von Windows NT zu Beginn des Projekts die folgenden Designziele auf: QQ Erweiterbarkeit Der Code muss so geschrieben werden, dass er bequem ergänzt und geändert werden kann, wenn sich die Marktanforderungen wandeln. QQ Portierbarkeit Das System muss auf verschiedenen Hardwarearchitekturen lauffähig und auch relativ einfach auf neue Architekturen übertragbar sein, wenn der Markt dies verlangt. 51
QQ Zuverlässigkeit und Stabilität Das System muss sich selbst sowohl gegen interne Fehlfunktionen als auch gegen externe Manipulation schützen. Es darf nicht möglich sein, dass Anwendungen das Betriebssystem oder andere Anwendungen schädigen. QQ Kompatibilität Windows NT soll zwar die vorhandene Technologie erweitern, seine Benutzerschnittstelle und die APIs sollen aber mit älteren Versionen von Windows und MSDOS kompatibel sein. Es soll auch gut mit anderen Systemen wie UNIX, OS/2 und NetWare zusammenwirken können. QQ Leistung Das System soll im Rahmen der Vorgaben durch die anderen Designziele auf jeder Hardwareplattform so schnell und reaktiv sein wie möglich. Wenn wir die internen Strukturen und Vorgänge von Windows untersuchen, werden Sie noch sehen, wie diese Designziele und die Marktanforderungen in der Konstruktion erfolgreich ver­ knüpft wurden. Bevor wir unsere Erforschung beginnen, wollen wir jedoch das allgemeine Designmodell von Windows betrachten und mit dem anderer moderner Betriebssysteme vergleichen. Das Modell des Betriebssystems In den meisten Mehrbenutzer-Betriebssystemen sind die Anwendungen vom Betriebssystem getrennt. Der Kernelcode des Betriebssystems läuft in einem privilegierten Prozessormodus (der in diesem Buch als Kernelmodus bezeichnet wird) und hat Zugriff auf die Systemdaten und die Hardware. Der Anwendungscode dagegen wird in einem nicht privilegierten Prozessormodus ausgeführt (dem Benutzermodus), in dem nur eine begrenzte Menge der Schnittstellen zur Verfügung steht, der Zugriff auf Systemdaten eingeschränkt ist und es keinen direkten Zugriff auf die Hardware gibt. Wenn ein Benutzermodusprogramm einen Systemdienst aufruft, führt der Prozesse eine besondere Anweisung aus, die den aufrufenden Thread in den Kernelmodus umschaltet. Hat der Systemdienst seine Arbeit abgeschlossen, schaltet das Betriebssystem den Threadkontext wieder in den Benutzermodus und erlaubt dem Aufrufer, fortzufahren. Windows ähnelt den meisten UNIX-Systemen in der Hinsicht, dass es sich um ein monoli­ thisches Betriebssystem handelt, d. h., dass sich der Großteil des Betriebssystems und der Gerätetreibercode denselben geschützten Kernelmodus-Arbeitsspeicherbereich teilen. Dadurch ist jede Betriebssystemkomponente und jeder Gerätetreiber theoretisch in der Lage, Daten zu beschädigen, die von anderen Systemkomponenten genutzt werden. Wie Sie in Kapitel 1 gesehen haben, geht Windows dieses Problem jedoch dadurch an, dass es die Qualität von Drittanbietertreibern stärkt und ihre Herkunft einschränkt. Dies wird mit Programmen wie WHQL erreicht und durch KMCS durchgesetzt. Außerdem enthält Windows weitere Kernelschutztechnologien wie die virtualisierungsgestützte Sicherheit sowie DeviceGuard und HyperGuard. Wie all diese Einzelteile zusammenpassen, schauen wir uns später in diesem Kapitel an. Weitere Einzelheiten erfahren Sie in Kapitel 7, »Sicherheit«, und Kapitel 8, »Systemmechanismen«, von Band 2. All diese Betriebssystemkomponenten sind vollständig gegen fehlgehende Anwendungen geschützt, da Anwendungen keinen direkten Zugriff auf den Code und die Daten im privilegierten Teil des Betriebssystems haben (wobei sie jedoch andere Kerneldienste aufrufen können). Dieser Schutz ist einer der Gründe, warum Windows einen Ruf als widerstandsfähige und 52 Kapitel 2 Systemarchitektur
stabile Plattform für Anwendungsserver und Arbeitsstationen hat. Gleichzeitig ist es, was die wichtigsten Betriebssystemdienste wie die Verwaltung des virtuellen Speichers, die Datei-E/A, Netzwerkkommunikation und Datei- und Druckerfreigabe angeht, schnell und behände. Die Kernelmoduskomponenten von Windows verkörpern auch die Grundprinzipien des objektorientierten Designs. Beispielsweise langen sie im Allgemeinen nicht in die Datenstrukturen anderer Komponenten hinein, um auf deren Informationen zuzugreifen, sondern nutzen formale Schnittstellen, um Parameter zu übergeben, auf Datenstrukturen zuzugreifen und diese zu bearbeiten. Auch wenn zur Darstellung gemeinsam genutzter Systemressourcen überall Objekte verwendet werden, ist Windows kein objektorientiertes System im strengen Sinne. Der meiste Kernelmoduscode des Betriebssystems ist zur leichteren Portierbarkeit in C geschrieben. Diese Programmiersprache unterstützt objektorientierte Konstrukte wie polymorphe Funktionen und Klassenvererbung nicht direkt. Daher borgt sich die C-Implementierung von Objekten in Windows einige Merkmale bestimmter objektorientierter Sprachen, hängt aber nicht davon ab. Die Architektur im Überblick Nach dem kurzen Überblick über die Designziele und das Modell von Windows wollen wir uns nun die Hauptkomponenten ansehen, die die Architektur ausmachen. Abb. 2–1 zeigt eine vereinfachte Darstellung der Architektur, aber nicht alles. Beispielsweise sind die Netzwerkkomponenten und die verschiedenen Gerätetreiberschichten hier nicht enthalten. Umgebungs­ teilsysteme System­ prozesse Dienst­ prozesse Benutzerprozesse Teilsystem-DLLs Benutzermodus NTDLL.DLL Kernelmodus Exekutive Fenster und Grafik Gerätetreiber Kernel Hardwareabstraktionsschicht (HAL) Hypervisor Hyper-V Kernelmodus (Hypervisor-Kontext) Abbildung 2–1 Vereinfachte Darstellung der Windows-Architektur In Abb. 2–1 sind die Teile von Windows für den Benutzer- und den Kernelmodus durch eine horizontale Linie getrennt. Wie bereits in Kapitel 1 erwähnt, werden Benutzermodusthreads in einem privaten Prozessadressraum ausgeführt (wobei sie jedoch bei der Ausführung im Kernelmodus auch Zugriff auf den Systemadressraum haben). Daher haben System-, Dienst- und Benutzerprozesse sowie die Umgebungsteilsysteme jeweils ihren eigenen privaten Prozess­ Die Architektur im Überblick Kapitel 2 53
adressraum. Es gibt auch noch eine zweite Trennlinie zwischen den Teilen von Windows für den Kernelmodus und dem Hypervisor. Der Hypervisor hat zwar dieselbe CPU-Rechteebene wie der Kernel (0), verwendet aber besondere CPU-Anweisungen (VT-x auf Intel, SVM auf AMD) und kann sich daher vom Kernel isolieren und ihn (und die Anwendungen) gleichzeitig überwachen. Daher werfen auch manche Leute mit dem Begriff Ring -1 um sich (der nicht korrekt ist). Es gibt die folgenden vier grundlegenden Arten von Benutzermodusprozessen: QQ Benutzerprozesse Dies können Prozesse der folgenden Typen sein: Windows 32 Bit oder 64 Bit (dazu gehören Windows-Apps, die oberhalb der Windows Runtime in Windows 8 und höher laufen), Windows 3.1 16 Bit, MS-DOS 16 Bit, POSIX 32 Bit und 64 Bit. Beachten Sie, dass 16-Bit-Anwendungen nur auf 32-Bit-Windows ausgeführt werden können und POSIX-Anwendungen ab Windows 8 nicht mehr unterstützt werden. QQ Dienstprozesse Dies sind Hostprozesse für Windows-Dienste, z. B. den Taskplaner und den Druckspooler. Dienste müssen generell unabhängig von Benutzeranmeldungen ausgeführt werden können. Viele Windows-Serveranwendungen, z. B. Microsoft SQL Server und Microsoft Exchange Server, enthalten ebenfalls Komponenten, die als Dienste ausgeführt werden. Eine ausführliche Beschreibung von Diensten finden Sie in Kapitel 9, »Verwaltungsmechanismen«, von Band 2. QQ Systemprozesse Dies sind unveränderliche oder fest verdrahtete Prozesse, z. B. der Anmeldeprozess und der Sitzungs-Manager. Es handelt sich dabei nicht um Windows-Dienste, d. h., sie werden nicht vom Dienststeuerungs-Manager gestartet. QQ Serverprozesse des Umgebungsteilsystems Diese Prozesse implementieren die Unterstützung für die Betriebssystemumgebung (oder »Persönlichkeit«), die dem Benutzer und Programmierer angezeigt wird. Windows NT enthielt ursprünglich drei Umgebungsteilsysteme, nämlich Windows, POSIX und OS/2. Allerdings wurde das OS/2-Teilsystem mit Windows 2000 zum letzten Mal ausgeliefert und das POSIX-Teilsystem mit Windows XP. Die Ultimate- und die Enterprise-Edition der Clientversion von Windows 7 sowie Windows Server 2008 R2 enthielten noch eine Unterstützung für ein erweitertes POSIX-Teilsystem namens SUA (Subsystem for UNIX-based Applications). SUA wurde jedoch eingestellt und wird auch nicht mehr optional für Windows angeboten (weder für Client- noch für Serverversionen). Windows 10 Version 1607 enthält ein Windows-Teilsystem für Linux (WSL), allerdings nur als Beta-Version ausschließlich für Entwickler. Es ist auch kein echtes Teilsystem wie die anderen in diesem Abschnitt. WSL und die zugehörigen Pico-Provider werden in diesem Kapitel noch ausführlicher beschrieben. Weitere Informationen über Pico-Prozesse erhalten Sie in Kapitel 3, »Prozesse und Jobs«. Beachten Sie in Abb. 2–1 den Kasten »Teilsystem-DLLs« unter »Dienstprozesse« und »Benutzerprozesse«. In Windows rufen Benutzeranwendungen die nativen Betriebssystemdienste nicht direkt auf, sondern nutzen eine oder mehrere Teilsystem-DLLs. Deren Aufgabe besteht darin, eine dokumentierte Funktion in die entsprechenden internen (und in der Regel nicht dokumentierten) nativen Systemdienstaufrufe zu übersetzen, die meistens in Ntdll.dll implementiert 54 Kapitel 2 Systemarchitektur
sind. Dabei kann eine Nachricht an den Prozess des Umgebungsteilsystems gesendet werden, der den Benutzerprozess handhabt. Windows enthält die folgenden Kernelmoduskomponenten: QQ Exekutive Die Windows-Exekutive umfasst die grundlegenden Betriebssystemdienste wie Speicherverwaltung, Prozess- und Threadverwaltung, Sicherheit, E/A, Netzwerk und Kommunikation zwischen Prozessen. QQ Der Windows-Kernel Er besteht aus den maschinennahen Betriebssystemfunktionen wie Threadplanung, Interrupt- und Ausnahmezuteilung und Multiprozessorsynchronisierung. Außerdem bietet er eine Reihe von Routinen und Grundobjekten, die der Rest der Exekutive verwendet, um Konstrukte höherer Ebene zu implementieren. QQ Gerätetreiber Dazu gehören sowohl Hardwaregerätetreiber, die E/A-Funktionsaufrufe des Benutzers in gerätespezifische E/A-Anforderungen übersetzen, aber auch Gerätetreiber nicht für Hardware, z. B. das Dateisystem und Netzwerktreiber. QQ Hardwareabstraktionsschicht (HAL) Diese Codeschicht schirmt den Kernel, die Gerätetreiber und den Rest der Windows-Exekutive gegen plattformspezifische Hardwareunterschiede ab (z. B. Unterschiede zwischen Motherboards). QQ Fenster- und Grafiksystem Dieses System implementiert die GUI-Funktionen (auch bekannt als Windows USER- und GDI-Funktionen) für den Umgang mit Fenstern, die Steuer­ elemente der Benutzerschnittstelle, das Zeichnen usw. QQ Hypervisor-Schicht Diese Schicht besteht aus einer einzigen Komponente, nämlich dem Hypervisor selbst. In dieser Umgebung gibt es keine Treiber oder anderen Module. Allerdings besteht der Hypervisor selbst aus mehreren internen Schichten und Diensten wie seinem eigenen Speicher-Manager, dem Scheduler für den virtuellen Prozessor, einer Interrupt- und Timerverwaltung, Synchronisierungsroutinen, einer Partitionsverwaltung (für die VM-Instanzen), Kommunikation zwischen den Partitionen (IPC) usw. Tabelle 2–1 nennt die Dateinamen der wichtigsten Windows-Komponenten (die Sie kennen müssen, da wir in diesem Buch mehrere Systemdateien namentlich erwähnen). Alle diese Komponenten werden in diesem oder den folgenden Kapiteln ausführlicher beschrieben. Datei Komponenten Ntoskrnl.exe Exekutive und Kernel Hal.dll HAL Win32k.sys Kernelmodusteil des Windows-Teilsystems (GUI) Hvix64.exe (Intel), Hvax64.exe (AMD) Hypervisor .sys-Dateien in \SystemRoot\System32\Drivers Haupttreiberdateien, darunter für Direct X, Volume-­ Manager, TCP/IP, TPM und ACPI-Unterstützung Ntdll.dll Interne Unterstützungsfunktionen und Dispatch-Stubs zur Zuteilung von Systemdiensten an Ausführungsfunktionen Kernel32.dll, Advapi32.dll, User32.dll, Gdi32.dll Haupt-DLLs des Windows-Teilsystems Tabelle 2–1 Wichtige Windows-Systemdateien Die Architektur im Überblick Kapitel 2 55
Bevor wir uns diese Systemkomponenten genauer ansehen, wollen wir jedoch noch einige Grundlagen des Windows-Kerneldesigns vorstellen. Dabei beginnen wir mit der Frage, wie Windows die Portierbarkeit über mehrere Hardwarearchitekturen hinweg erreicht. Portierbarkeit Windows wurde so entworfen, dass es auf verschiedenen Hardwarearchitekturen laufen kann. Das ursprüngliche Release von Windows NT unterstützte x86 und MIPS. Kurz darauf folgte die Unterstützung für Alpha AXP von Digital Equipment Corporation (die von Compaq aufgekauft wurde, das wiederum mit Hewlett-Packard fusionierte). (Alpha AXP war ein 64-Bit-Prozessor, doch Windows NT lief im 32-Bit-Modus. Während der Entwicklung von Windows 2000 wurde auch eine native 64-Bit-Version auf dem Alpha AXP ausgeführt, allerdings wurde sie niemals veröffentlicht.) In Windows NT 3.51 kam die Unterstützung für eine vierte Prozessorarchitektur hinzu, nämlich den Motorola PowerPC. Aufgrund wechselnder Nachfrage am Markt wurde die Unterstützung für MIPS und PowerPC jedoch aufgegeben, bevor die Entwicklung von Windows 2000 begann. Spät nahm Compaq die Unterstützung für die Alpha AXP-Architektur zurück, sodass Windows 2000 nur noch auf x86 lief. In Windows XP und Windows Server 2003 kam die Unterstützung für zwei 64-Bit-Prozessorfamilien hinzu, den Intel Itanium IA-64 und den AMD64 mit der gleichwertigen 64-Bit-Erweiterungstechnologie von Intel (EM64T). Die beiden letztgenannten Implementierungen wurden als erweiterte 64-Bit-Systeme bezeichnet und werden in diesem Buch x64 genannt. (Wie 32-Bit-Anwendungen auf 64-Bit-Windows ausgeführt werden konnten, wird in Kapitel 8 von Band 2 erklärt.) Ab Server 2008 R2 wurden IA-64-Systeme jedoch nicht mehr von Windows unterstützt. Bei jüngeren Windows-Editionen kam auch eine Unterstützung für die ARM-Prozessorarchitektur hinzu. Beispielsweise handelte es sich bei Windows RT um eine Version von Windows 8 für ARM, die inzwischen allerdings eingestellt wurde. Windows 10 Mobile – der Nachfolger von Windows Phone 8.x – läuft auf ARM-Prozessoren wie den Snapdragon-Modellen von Qualcomm. Windows 10 IoT kann sowohl auf x86- als auch auf ARM-Geräten wie dem Raspberry Pi 2 (mit dem Prozessor ARM Cortex-A7) und dem Raspberry Pi 3 (ARM Cortex-A53) ausgeführt werden. Da es inzwischen auch 64-Bit-ARM-Hardware gibt und eine steigende Anzahl von Geräten die Prozessoren der neuen Familien AArch64 und ARM64 verwendet, wird es wahrscheinlich irgendwann auch eine Unterstützung dafür geben. Die architektur- und plattformübergreifende Portierbarkeit erreicht Windows auf allem auf die beiden folgenden Weisen: QQ Geschichtetes Design Windows ist in Schichten aufgebaut. Dabei stehen die maschinennahen Teile des Systems, die von der Prozessorarchitektur oder der Plattform abhängen, in getrennten Modulen, sodass die höheren Ebenen des Systems von den Unterschieden zwischen diesen Architekturen und Plattformen abgeschirmt sind. Die beiden Hauptkomponenten, die für die Portierbarkeit sorgen, sind der Kernel (in Ntoskrnl.exe) und die HAL (in Hal.dll). Beide Komponenten werden weiter hinten in diesem Kapitel noch ausführlicher behandelt. Architekturspezifische Funktionen wie Threadkontextwechsel und Trap-Dispatching sind im Kernel implementiert, und Funktionen, die sich auf Systemen mit derselben Architektur unterscheiden können (z. B. auf unterschiedlichen Motherboards) 56 Kapitel 2 Systemarchitektur
in der HAL. Die einzige andere Komponente mit einem erheblichen Anteil an architekturspezifischem Code ist der Speicher-Manager, aber im Vergleich zum Gesamtsystem ist dieser Anteil sehr gering. Der Hypervisor weist einen ähnlichen Aufbau auf: Die meisten Teile werden von der AMD- und der Intel-Implementierung (SVM bzw. VT-x) gemeinsam genutzt, und daneben gibt es noch einige wenige prozessorspezifische Teile (daher auch die beiden Dateinamen in Tabelle 2–1). QQ Verwendung von C Der Großteil von Windows ist in C geschrieben, einige Teile in C++. Eine Assemblersprache wird nur für die Teile des Betriebssystems verwendet, die direkt mit der Systemhardware kommunizieren müssen (z. B. Interrupt-Trap-Handler) oder die äußerst leistungsempfindlich sind (z. B. Kontextwechsel). Assemblercode gibt es nicht nur im Kernel und in der HAL, sondern auch an einigen anderen Stellen des Kernbetriebssystems (z. B. in den Routinen für gesperrte Anweisungen und in einem Modul in der Einrichtung für lokale Prozeduraufrufe), im Kernelmodus-Teil des Windows-Teilsystems und sogar in einigen Benutzermodus-Bibliotheken, darunter im Prozessstartcode in Ntdll.dll (einer Systembibliothek, die weiter hinten in diesem Kapitel noch genauer beschrieben wird). Symmetrisches Multiprocessing Multitasking ist eine Betriebssystemtechnik, mit der sich mehrere Ausführungsthreads einen einzigen Prozessor teilen können. Auf einem Computer mit mehreren Prozessoren können jedoch mehrere Threads gleichzeitig ausgeführt werden. Während ein Multitasking-Betriebssystem nur vorgibt, mehrere Threads gleichzeitig auszuführen, führt ein Multiprocessing-Betriebssystem jedoch tatsächlich eine Mehrfachverarbeitung durch und führt auf jedem der Prozessoren einen Thread aus. Wie zu Anfang dieses Kapitels erwähnt, bestand eines der wichtigsten Designziele für Windows darin, dass es auch auf Mehrprozessorsystemen gut laufen sollte. Windows ist ein Betriebssystem mit symmetrischem Multiprocessing (SMP). Es gibt keinen Masterprozessor – das Betriebssystem und alle Benutzerthreads können auf beliebigen Prozessoren ausgeführt werden. Außerdem teilen sich alle Prozessoren denselben Speicherbereich. Das steht im Gegensatz zum asymmetrischen Multiprocessing (ASMP), bei dem das Betriebssystem gewöhnlich einen Prozessor für die Ausführung des Kernelcodes auswählt, während auf den anderen Prozessoren nur Benutzercode läuft. Die Unterschiede dieser beiden Modelle veranschaulicht Abb. 2–2. Die Architektur im Überblick Kapitel 2 57
Symmetrisch Asymmetrisch Arbeitsspeicher CPU A Arbeitsspeicher CPU B Betriebs­ system­ thread CPU A Benutzer­ thread Benutzer­ thread Benutzer­ thread Betriebs­ system­ thread CPU B Betriebs­ system­ thread Benutzer­ thread Betriebs­ system­ thread Benutzer­ thread Benutzer­ thread Benutzer­ thread Benutzer­ thread Abbildung 2–2 Symmetrisches und asymmetrisches Multiprocessing im Vergleich Windows unterstützt vier moderne Arten von Mehrprozessorsystemen: Mehrkernsysteme, SMT-Systeme (Simultaneous Multi-Threaded), heterogene Systeme und NUMA-Systeme (Non-Uniform Memory Access). Sie werden in den folgenden Absätzen kurz vorgestellt. (Eine vollständige, ausführliche Beschreibung der Unterstützung für die Threadplanung auf solchen Systemen finden Sie im entsprechenden Abschnitt in Kapitel 4, »Threads«.) SMT wurde durch die Unterstützung für die Hyperthreading-Technologie von Intel in Windows eingeführt, mit der für jeden physischen Prozessorkern zwei logische Prozessoren bereitgestellt werden können. Neuere AMD-Prozessoren mit Zen-Mikroarchitektur nutzen eine ähnliche SMT-Technik, um ebenfalls die Anzahl der logischen Prozessoren zu verdoppeln. Jeder logische Prozessor hat seinen eigenen CPU-Status, aber die Ausführungs-Engine und der Onboard-Cache werden gemeinsam genutzt. Dadurch kann eine logische CPU weiterarbeiten, während die andere angehalten ist (z. B. nach einem Cachefehler oder einer falsch vorausgesagten Verzweigung). In ihrem Marketingmaterial nennen beide Firmen die zusätzlichen Kerne leider Threads, weshalb man oft Behauptungen wie »vier Kerne, acht Threads« liest. Das bedeutet, dass bis zu acht Threads geplant werden können, es also acht logische Prozessoren gibt. Die Scheduler-Algorithmen wurden verbessert, um die Vorteile von SMT-fähigen Computern optimal zu nutzen, z. B. durch Planung von Threads auf leerlaufenden physischen Prozessoren, anstatt einen leerlaufenden logischen Prozessor auf einem physischen Prozessor auszuwählen, dessen andere logische Prozessoren beschäftigt sind. Weitere Einzelheiten über die Threadplanung finden Sie in Kapitel 4. Auf NUMA-Systemen sind die Prozessoren zu kleineren Einheiten gruppiert, die als Knoten bezeichnet werden. Jeder Knoten verfügt über seine eigenen Prozessoren und seinen eigenen Arbeitsspeicher und ist über einen cachekohärenten Bus mit dem Gesamtsystem verbunden. Auf NUMA läuft Windows immer noch als SMP-System in dem Sinne, dass alle Prozessoren Zugriff auf den gesamten Arbeitsspeicher haben. Allerdings kann der Knoten schneller auf seinen lokalen Arbeitsspeicher zugreifen als auf den Arbeitsspeicher anderer Knoten. Daher versucht das System, die Leistung dadurch zu steigern, dass es Threads auf Prozessoren einplant, die 58 Kapitel 2 Systemarchitektur
zum selben Knoten gehören wie der verwendete Arbeitsspeicher. Es bemüht sich, Speicherzuweisungsanforderungen innerhalb des Knotens zu erfüllen, weist aber auch Arbeitsspeicher auf anderen Knoten zu, wenn dies notwendig sein sollte. Windows bietet eine native Unterstützung für Mehrkernsysteme. Da sie über mehrere physische Kerne (im selben Gehäuse) verfügen, behandelt der SMP-Code von Windows sie als getrennte Prozessoren. Eine Ausnahme davon bilden bestimmte Buchführungs- und Identifizierungsaufgaben (wie die in Kürze beschriebene Lizenzierung), bei denen zwischen Kernen auf demselben Prozessor und Kernen auf verschiedenen Sockeln unterschieden wird. Das ist insbesondere beim Umgang mit Cachetopologien zur Optimierung der gemeinsamen Datennutzung wichtig. Die ARM-Versionen von Windows unterstützen auch das sogenannte heterogene Multiprocessing, dessen Implementierung auf solchen Prozessoren als big.LITTLE bezeichnet wird. Diese Form des SMP-Designs weicht von der herkömmlichen darin ab, dass die Prozessorkerne nicht alle die gleichen Fähigkeiten haben, aber anders als reines heterogenes Multiprocessing immer noch in der Lage sind, dieselben Anweisungen auszuführen. Der Unterschied liegt in den Takt­ raten und dem Verhältnis des Stromverbrauchs bei Volllast und Leerlauf, wodurch langsame Kerne mit schnelleren kombiniert werden können. Stellen Sie sich vor, Sie würden auf einem älteren Zweikernsystem mit 1 GHz, das an eine moderne Internetverbindung angeschlossen ist, eine E-Mail senden. Der Vorgang würde wahrscheinlich nicht langsamer ablaufen als auf einem 8-Kern-Rechner mit 3,6 GHz, da nicht die Rechenleistung den entscheidenden Engpass bildet, sondern die Tippgeschwindigkeit eines Menschen und die Netzwerkbandbreite. Auf der anderen Seite kann ein modernes System selbst im sparsamsten Energiesparmodus noch erheblich mehr Strom verbrauchen als ein älteres System. Das gilt selbst dann, wenn sich das moderne System selbst auf 1 GHz herunterregeln könnte, denn dann hätte sich das alte System vermutlich auch schon auf 200 MHz herabgestuft. Durch die Möglichkeit, solche alten Prozessoren mit Spitzenmodellen zu kombinieren, kann eine ARM-Plattform mit einem kompatiblen Kernelscheduler die Verarbeitungsleistung maximieren, wenn es erforderlich ist (indem alle Kerne eingeschaltet werden), in einem extremen Energiesparmodus laufen (indem nur ein einziger kleiner Kern online gehalten wird, was für SMS und das Senden von Push-E-Mail ausreicht) oder einen Ausgleich finden (indem einige große Kerne und für andere Aufgaben einige kleine online sind). Durch die Unterstützung der Richtlinien für heterogenes Scheduling können Threads in Windows 10 die Richtlinie auswählen, die ihre Bedürfnisse am besten erfüllt, und mit dem am besten geeigneten Scheduler und Energie-Manager arbeiten. Mehr über diese Richtlinien erfahren Sie in Kapitel 4. Windows wurde nicht im Hinblick auf eine bestimmte Anzahl von Prozessoren entworfen. Aus Gründen des Komforts und der Effizienz hält Windows jedoch Informationen über die Prozessoren (Gesamtzahl, Prozessoren im Leerlauf, beschäftigte Prozessoren usw.) in einer Bitmaske (oder Affinitätsmaske) fest. Die Bitanzahl dieser Maske entspricht dem nativen Datentyp des Computers (32 Bit oder 64 Bit), sodass der Prozessor die Bits direkt in einem Register bearbeiten kann. Daher waren Windows-Systeme ursprünglich auf die Anzahl der CPUs beschränkt, die in ein natives Word passt, da sich die Affinitätsmaske nicht willkürlich vergrößern ließ. Um die Kompatibilität zu erhalten und auch Systeme mit mehr Prozessoren zu unterstützen, implementiert Windows ein Konstrukt höherer Ordnung, nämlich die Prozessorgruppe. Dabei handelt es sich um einen Satz von Prozessoren, die durch eine einzige Affinitätsmaske defi- Die Architektur im Überblick Kapitel 2 59
niert werden können. Bei Änderungen der Affinität können der Kernel und die Anwendungen wählen, auf welche Gruppe sie sich beziehen. Kompatible Anwendungen können die Anzahl der unterstützten Gruppen abfragen (zurzeit bis zu 20, wobei die maximale Anzahl logischer Prozessoren zurzeit bei 640 liegt) und dann die Bitmasken für die einzelnen Gruppen aufzählen. Ältere Anwendungen sehen nur ihre aktuelle Gruppe, sodass sie ebenfalls funktionieren. Wie Windows Prozessoren zu Gruppen zuweist (was auch im Zusammenhang mit NUMA steht), erfahren Sie in Kapitel 4. Die tatsächliche Anzahl der unterstützten lizenzierten Prozessoren hängt von der verwendeten Windows Edition ab (siehe Tabelle 2–2 weiter hinten). Diese Anzahl ist in der Lizenzrichtliniendatei %SystemRoot%\ServiceProfiles\LocalService\AppData\Local\Microsoft\WSLicense\tokens. dat in der Variablen kernel-RegisteredProcessors gespeichert (im Grunde genommen eine Liste von Schlüssel-Wert-Paaren). Skalierbarkeit Eines der Hauptprobleme von Multiprozessorsystemen ist die Skalierbarkeit. Um auf SMP-Systemen korrekt ausgeführt werden zu können, muss der Betriebssystemcode strenge Richtlinien und Regeln erfüllen. Der Wettstreit um Ressourcen und andere Leistungsprobleme sind auf Mehrprozessorsystemen komplizierter als auf Einzelprozessorsystemen und müssen beim Design des Systems berücksichtigt werden. Windows weist mehrere Merkmale auf, die für seinen Erfolg als Multiprozessor-Betriebssystem von entscheidender Bedeutung waren: QQ Die Möglichkeit, Betriebssystemcode auf einem beliebigen verfügbaren Prozessor und auch gleichzeitig auf mehreren Prozessoren auszuführen QQ Mehrere Ausführungsthreads in einem Prozess, die alle gleichzeitig auf verschiedenen Prozessoren laufen können QQ Feine Synchronisierung im Kernel (z. B. Spinlocks, Warteschlangen-Spinlocks und Pushlocks, wie sie in Kapitel 8 von Band 2 beschrieben werden) und in Gerätetreibern und Serverprozessen, wodurch mehr Komponenten gleichzeitig auf mehreren Prozessoren laufen können QQ Programmiermechanismen wie E/A-Vervollständigungsports (siehe Kapitel 6, »Das E/A-­ System«) für eine effiziente Implementierung von Multithread-Serverprozessen, die sich auf Mehrprozessorsystemen gut skalieren lassen Die Skalierbarkeit des Windows-Kernels hat sich im Laufe der Zeit entwickelt. Beispielsweise wurden Planungswarteschlangen für einzelne CPUs mit feinen Sperren in Windows Server 2003 eingeführt, was es ermöglichte, Entscheidungen zur Threadplanung parallel auf mehreren Prozessoren durchzuführen. In Windows 7 und Windows Server 2008 R2 wurde die globale Schedulersperre bei Warte-Dispatchoperationen beseitigt. Diese schrittweise Verfeinerung der Sperrmechanismen hat auch in anderen Bereichen stattgefunden, z. B. im Speicher-, im Cacheund im Objekt-Manager. 60 Kapitel 2 Systemarchitektur
Unterschiede zwischen den Client- und Serverversionen Windows wird sowohl als Client- als auch als Serverversion verkauft. Von Windows 10 gibt es sechs Desktop-Clientversionen: Windows 10 Home, Windows 10 Pro, Windows 10 Education, Windows 10 Pro Education, Windows 10 Enterprise und Windows 10 Enterprise Long Term Serving Branch (LTSB). Des Weiteren sind Editionen für andere Geräte als Desktopcomputer erhältlich, darunter Windows 10 Mobile, Windows 10 Mobile Enterprise, Windows 10 IoT Core, IoT Core Enterprise und IoT Mobile Enterprise. Für den besonderen Bedarf einzelner Regionen gibt es noch mehr Varianten, etwa die N-Serie. Von Windows Server 2016 gibt es ebenfalls sechs Versionen: Windows Server 2016 Datacenter, Windows Server 2016 Standard, Windows Server 2016 Essentials, Windows Server 2016 MultiPoint Premium Server, Windows Storage Server 2016 und Microsoft Hyper-V Server 2016. Zwischen den Versionen bestehen folgende Unterschiede: QQ Preisgestaltung nach Anzahl der Kerne (statt der Prozessorsockel) bei Windows Server 2016 Datacenter und Standard QQ Anzahl der insgesamt unterstützten logischen Prozessoren QQ Anzahl der Hyper-V-Container, die ausgeführt werden dürfen (bei Serversystemen; in Clientsystemen können nur Windows-Container auf der Grundlage getrennter Namespaces verwendet werden) QQ Unterstützte Größe des physischen Arbeitsspeichers (eigentlich die höchste für den RAM nutzbare physische Adresse; mehr über Grenzwerte für den physischen Arbeitsspeicher erfahren Sie in Kapitel 5, »Speicherverwaltung«) QQ Anzahl der zugelassenen gleichzeitigen Netzwerkverbindungen (beispielsweise sind in den Clientversionen höchstens zehn gleichzeitige Verbindungen für Datei- und Druckdienste erlaubt) QQ Unterstützung für Multitouch und Desktopgestaltung QQ Unterstützung für Merkmale wie BitLocker, VHD-Start, AppLocker, Hyper-V und über 100 weitere einstellbare Lizenzrichtlinienwerte QQ Geschichtete Dienste, die in den Server-, aber nicht in den Clientversionen verfügbar sind (z. B. Verzeichnisdienste, Host-Überwachungsdienst, direkte Speicherplätze, abgeschirmte virtuelle Maschinen und Clusterdienste) Tabelle 2–2 nennt die Unterschiede bei der Unterstützung für Arbeitsspeicher und Prozessoren zwischen einigen Editionen von Windows 10, Windows Server 2012 R2 und Windows Server 2016. Eine ausführliche Vergleichstabelle für die verschiedenen Ausgaben von Windows Server 2012 R2 finden Sie auf https://www.microsoft.com/en-us/download/details.aspx?id=41703. Speichergrenzwerte der Editionen von Windows 10 und Server 2016 sowie früherer Betriebssysteme sind auf https://msdn.microsoft.com/en-us/library/windows/desktop/aa366778.aspx aufgeführt. Die Client- und Servereditionen werden zwar in verschiedenen Paketen verkauft, nutzen aber alle die gleichen Hauptsystemdateien, darunter das Kernelabbild Ntoskrnl.exe (und die Die Architektur im Überblick Kapitel 2 61
PAE-Version Ntkrnlpa.exe), die HAL-Bibliotheken, die Gerätetreiber, die grundlegenden Systemprogramme und die DLLs. Anzahl Prozessorsockel (32-Bit-Edition) Physischer Arbeitsspeicher (32-Bit-Edition) Anzahl logischer Prozessoren/ Sockel (64-BitEdition) Physischer Arbeitsspeicher (x64-Editionen) Windows 10 Home 1 4 GB 1 Sockel 128 GB Windows 10 Pro 2 4 GB 2 Sockel 2 TB Windows 10 Enterprise 2 4 GB 2 Sockel 2 TB Windows Server 2012 R2 Essentials – – 2 Sockel 64 GB Windows Server 2016 Standard – – 512 logische Prozessoren 24 TB Windows Server 2016 Datacenter – – 512 logische Prozessoren 24 TB Tabelle 2–2 Prozessor- und Speichergrenzwerte für einige Windows-Editionen Woher weiß das System angesichts so vieler verschiedener Editionen, die alle dasselbe Kernelabbild verwenden, welche Edition gestartet wurde? Dazu fragt es die Registrierungswerte ProductType und ProductSuite unter dem Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\ ProductOptions ab. Dabei dient ProductType zur Unterscheidung zwischen Client- und Serversystem (gleich welcher Variante). In die Registrierung geladen werden diese Werte auf der Grundlage der zuvor beschriebenen Lizenzrichtliniendatei. Tabelle 2–3 führt die gültigen Werte auf. Die Abfrage kann im Benutzermodus durch die Funktion VerifyVersionInfo erfolgen oder von einem Gerätetreiber mit den Kernelmodusfunktionen RtlGetVersion und RtlVerifyVersionInfo durchgeführt werden, die beide im Windows Driver Kit dokumentiert sind. Windows-Edition Wert von ProductType Windows-Client WinNT Windows-Server (Domänencontroller) LanmanNT Windows-Server (reiner Server) ServerNT Tabelle 2–3 Registrierungswerte für ProductType Der Registrierungswert ProductPolicy enthält eine zwischengespeicherte Kopie der Daten in der Datei tokens.dat, die zwischen den Windows-Editionen und den von diesen jeweils aktivierten Funktionen unterscheidet. Wenn die Hauptdateien der Client- und Serverversionen im Grunde genommen identisch sind, wie unterscheiden sich die Systeme dann im Betrieb? Kurz Serversysteme sind standardmäßig für den Durchsatz von Hochleistungs-Anwendungsservern optimiert, wohingegen die Clientversionen (auch wenn sie einige Serverfähigkeiten haben) auf kurze Reaktionszeiten für interaktive Desktopbenutzung ausgelegt sind. So werden beispielsweise beim Hochfahren des 62 Kapitel 2 Systemarchitektur
Systems je nach Produkttyp unterschiedliche Entscheidungen zur Ressourcenzuweisung getroffen, z. B. was die Größe und Anzahl der Betriebssystemheaps (oder -pools), die Anzahl der internen Systemarbeitsthreads und die Größe des Systemdatencache angeht. Auch einige Laufzeitentscheidungen, z. B. die Abwägung des Speicher-Managers zwischen den Speicheranforderungen des Systems und der Prozesse, unterscheiden sich zwischen den Server- und den Clienteditionen. Sogar einige Einzelheiten der Threadplanung weisen in den beiden Familien jeweils ein unterschiedliches Standardverhalten auf (die Standardlänge der Zeitscheibe oder das Threadquantum; siehe Kapitel 4). Wenn es auf einem Gebiet erhebliche Unterschiede zwischen den beiden Produkten gibt, so wird in den entsprechenden Kapiteln dieses Buches jeweils darauf hingewiesen. Ansonsten gilt alles, was in diesem Buch beschrieben wird, sowohl für die Client- als auch die Serverversionen. Experiment: Die von der Lizenzierungsrichtlinie aktivierten Merkmale ermitteln Wie bereits erwähnt, unterstützt Windows mehr als 100 Merkmale, die durch den Softwarelizenzierungsmechanismus aktiviert werden. Diese Richtlinieneinstellungen bestimmen verschiedene Unterschiede nicht nur zwischen einer Client- und einer Serverinstallation, sondern auch zwischen den einzelnen Editionen (oder SKU) des Betriebssystems, z. B. die Unterstützung für BitLocker (verfügbar in der Serverversion und in den Editionen Pro und Enterprise der Clientversion). Mit dem Tool SlPolicy aus den Downloads zu diesem Buch können Sie viele dieser Richtlinienwerte anzeigen lassen. Die Richtlinieneinstellungen sind nach Einrichtungen (»facilities«) geordnet, also nach den Besitzermodulen, auf die die Richtlinie angewendet wird. Um eine Liste aller Einrichtungen einzusehen, die das Tool kennt, führen Sie SlPolicy.exe mit dem Schalter -f aus: C:\>SlPolicy.exe -f Software License Policy Viewer Version 1.0 (C)2016 by Pavel Yosifovich Desktop Windows Manager Explorer Fax Kernel IIS ... Wenn Sie den Namen einer Einrichtung hinter dem Schalter angeben, wird der Richtlinienwert für diese Einrichtung angezeigt. Wollen Sie sich beispielsweise die Grenzwerte für CPUs und den Arbeitsspeicher ansehen, so müssen Sie die Kernel-Einrichtung verwenden. Auf einem Computer mit Windows 10 Pro ergibt sich dabei folgende Ausgabe: C:\>SlPolicy.exe -f Kernel Software License Policy Viewer Version 1.0 (C)2016 by Pavel Yosifovich Kernel ------ → Die Architektur im Überblick Kapitel 2 63
Maximum allowed processor sockets: 2 Maximum memory allowed in MB (x86): 4096 Maximum memory allowed in MB (x64): 2097152 Maximum memory allowed in MB (ARM64): 2097152 Maximum physical page in bytes: 4096 Device Family ID: 3 Native VHD boot: Yes Dynamic Partitioning supported: No Virtual Dynamic Partitioning supported: No Memory Mirroring supported: No Persist defective memory list: No Bei Windows Server 2012 R2 Datacenter Edition dagegen sieht das Ergebnis wie folgt aus: Kernel -----Maximum allowed processor sockets: 64 Maximum memory allowed in MB (x86): 4096 Maximum memory allowed in MB (x64): 4194304 Add physical memory allowed: Yes Add VM physical memory allowed: Yes Maximum physical page in bytes: 0 Native VHD boot: Yes Dynamic Partitioning supported: Yes Virtual Dynamic Partitioning supported: Yes Memory Mirroring supported: Yes Persist defective memory list: Yes Testbuild Es gibt eine besondere interne Debugversion von Windows, die als Testbuild (oder manchmal auch überprüfter Build) bezeichnet wird. (Extern erhältlich ist sie nur für Windows 8.1 und früher und nur mit MSDN-Abonnement.) Dabei handelt es sich um eine Neukompilierung des Windows-Quellcodes mit dem Flag DBG, das dafür sorgt, dass Kompilierungszeitcode, Code für bedingtes Debugging und Ablaufverfolgungscode eingeschlossen werden. Um den Maschinencode leichter verständlich zu machen, erfolgt keine Nachbearbeitung der Windows-Binärdateien, die sonst zur Optimierung des Codelayouts und damit für eine schnelle Ausführung durchgeführt wird (siehe den Abschnitt »Debugging performance-optimized code« in der Hilfedatei zu den Debugging Tools for Windows). Der Testbuild ist hauptsächlich als Hilfestellung für die Entwickler von Gerätetreibern gedacht, da er eine strengere Fehlerprüfung von Kernelmodusfunktionen durchführt, die von Gerätetreibern oder anderem Systemcode aufgerufen werden. Wenn beispielsweise ein Treiber (oder anderer Kernelmoduscode) eine Systemfunktion auf ungültige Weise aufruft (indem er etwa ein 64 Kapitel 2 Systemarchitektur
Spinlock auf der falschen Interrupt-Anforderungsebene erwirbt) und das Problem erkannt wird, da die Funktion die Parameter prüft, so hält das System die Ausführung an, damit keine Datenstrukturen beschädigt werden, was später zu einem Absturz führen könnte. Da ein kompletter überprüfter Build häufig instabil ist und in den meisten Umgebungen nicht ausgeführt werden kann, stellt Microsoft ab Windows 10 nur einen überprüften Kernel mit HAL zur Verfügung. Dadurch können Entwickler diese Einrichtungen nutzen, ohne sich mit den Problemen beschäftigen zu müssen, die ein vollständiger überprüfter Build hervorruft. Der überprüfte Kernel und die überprüfte HAL stehen kostenlos im WDK zur Verfügung, und zwar im Verzeichnis \Debug des Stamminstallationspfads. Ausführliche Anleitungen zu ihrer Nutzung erhalten Sie im Abschnitt »Installing Just the Checked Operating System and HAL« in der WDK-Dokumentation. Experiment: Wird der Testbuild ausgeführt? Im Lieferumfang von Windows ist kein Werkzeug enthalten, mit dem Sie bestimmen können, ob Sie den Testbuild oder den Einzelhandelsbuild (freier Build) des Kernels ausführen. Allerdings ist diese Information über die Eigenschaft Debug der WMI-Klasse Win32_OperatingSystem verfügbar (Windows Management Instrumentation). Mit dem folgenden PowerShell-Skript können Sie diese Eigenschaft anzeigen lassen. (Das können Sie ausprobieren, indem Sie einen PowerShell-Skripthost öffnen.) PS C:\Users\pavely> Get-WmiObject win32_operatingsystem | select debug debug ----False In diesem Beispiel hat die Eigenschaft Debug den Wert False, was bedeutet, dass nicht der Testbuild ausgeführt wird. Eine Menge des zusätzlichen Codes in den Binärdateien des Testbuilds kommt durch die Makros ASSERT und NT_ASSERT zustande, die in der WDK-Headerdatei Wdm.h definiert sind und in der WDK-Dokumentation erklärt werden. Diese Makros testen eine Bedingung, z. B. die Gültigkeit einer Datenstruktur oder eines Parameters. Lautet das Ergebnis FALSE, so rufen die Makros entweder die Kernelmodusfunktion RtlAssert auf, die wiederum DbgPrintEx aufruft, um den Text der Debugnachricht an den Debugnachrichtenpuffer zu senden, oder sie geben einen Assertion-Interrupt aus, bei dem es sich auf x64- und x86-Systemen um den Interrupt 0x2B handelt. Ist ein Kerneldebugger angeschlossen und sind die entsprechenden Symbole geladen, so wird die Nachricht automatisch angezeigt und der Benutzer gefragt, was wegen des Assertionfehlers unternommen werden soll (Haltepunkt setzen, ignorieren, Prozess abbrechen oder Thread abbrechen). Ist das System ohne Kerneldebugger gestartet worden (also ohne die Option debug in der Bootkonfigurationsdatenbank) und ist daher kein Kerneldebugger angeschlossen, so führt der Fehlschlag eines Assertiontests zur Fehlerprüfung (und zum Absturz) des Systems. Eine Liste der Assertionchecks, die einige der Kernelunterstützungsroutinen durchführen, finden Sie im Abschnitt »Checked Build ASSERTs« in der WDK-Dokumentation (allerdings wird diese Liste nicht mehr gepflegt und ist veraltet). Die Architektur im Überblick Kapitel 2 65
Der überprüfte Build kann auch sehr nützlich zum Testen von Benutzermoduscode sein, da das System bei diesem Build ein anderes Timing hat. (Das liegt daran, dass im Kernel zusätzliche Überprüfungen erfolgen und die Komponenten ohne Optimierung kompiliert worden sind.) Oft hängen Fehler bei der Synchronisierung mehrerer Threads von bestimmten Timingbedingungen ab. Wenn Sie Ihre Tests auf einem System mit einem überprüften Build ausführen (oder zumindest mit überprüftem Kernel und HAL), dann können aufgrund des veränderten Systemtimings Bugs zutage treten, die sich auf einem normalen System nicht zeigen. Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick Durch die Trennung zwischen Benutzer- und Kernelmodus wird das Betriebssystem von Benutzermoduscode abgeschirmt, ob gefährlich oder nicht. Wenn unerwünschter Kernelmoduscode es jedoch ins System schafft (durch eine noch nicht gepatchte Schwachstelle im Kernel oder einem Treiber oder weil der Benutzer dazu verführt wurde, einen schädlichen oder angreifbaren Treiber zu installieren), ist das System im Grunde genommen bereits geknackt, da jeglicher Kernmoduscode Vollzugriff auf das gesamte System hat. Die in Kapitel 1 beschriebenen Technologien, die den Hypervisor nutzen, um zusätzlichen Schutz gegen Angriffe zu bieten, bilden zusammen die virtualisierungsgestützten Sicherheitseinrichtungen (Virtualization-Based Security, VBS), die die normale, vom Prozessor durchgeführte Trennung aufgrund von Rechten durch virtuelle Vertrauensebenen (Virtual Trust Levels, VTLs) erweitert. VTLs bieten nicht nur eine neue, orthogonale Möglichkeit, um den Zugriff auf Speicher-, Hardware- und Prozessorressourcen zu isolieren, sondern erfordern auch, dass neuer Code und neue Komponenten mit den höheren Vertrauensebenen umgehen können. Der reguläre Kernel und die regulären Treiber laufen auf VTL 0, weshalb ihnen nicht gestattet werden darf, VTL-1-Ressourcen zu steuern und zu definieren. Abb. 2–3 zeigt die Architektur von Windows 10 Enterprise und Windows Server 2016 mit aktiver VBS. (Manchmal wird auch die Bezeichnung Virtual Secure Mode oder kurz VSM verwendet.) Bei Windows 10 Version 1607 und Server 2016 ist VBS standardmäßig aktiv, wenn die Hardware dies zulässt. Bei älteren Versionen von Windows 10 können Sie VBS über eine Richtlinie oder im Dialogfeld Hinzufügen von Windows-Funktionen aktivieren (wählen Sie dazu die Option Isolierter Benutzermodus). 66 Kapitel 2 Systemarchitektur
VTL 0 VTL 1 Benutzermodus Isolierter Benutzermodus (IUM) Kernelmodus Sicherer Kernel Hypervisor Hyper-V SLAT Hardware I/O MMU Abbildung 2–3 VBS-Architektur von Windows 10 und Server 2016 Wie Sie in Abb. 2–3 sehen, läuft der zuvor beschriebene Benutzer- und Kernelcode wie schon in Abb. 2–1 oberhalb des Hypervisors Hyper-V. Der Unterschied besteht darin, dass es bei aktivierter VBS jetzt die VTL 1 gibt, die einen eigenen sicheren Kernel im privilegierten Prozessormodus (Ring 0 auf x86/x64) enthält. Ebenso gibt es mit dem isolierten Benutzermodus (IUM) jetzt einen Laufzeit-Benutzermodus, der im nicht privilegierten Modus (Ring 3) läuft. Bei dieser Architektur befindet sich der sichere Kernel in seiner eigenen, separaten Binärdatei, die als securekernel.exe auf der Festplatte steht. Der IUM ist sowohl eine Umgebung, die zulässige Systemaufrufe regulärer Benutzermodus-DLLs beschränkt (und damit auch, welche dieser DLLs geladen werden können), als auch ein Framework, das besondere, nur auf VTL 1 ausführbare sichere Systemaufrufe hinzufügt. Diese zusätzlichen Systemaufrufe werden auf ähnliche Weise verfügbar gemacht wie die regulären, nämlich durch eine interne Systembibliothek namens lumdll.dll (die VTL-1-Version von Ntdll.dll) und eine Bibliothek für das Windows-Teilsystem namens lumbase.dll (die VTL-Version von Kernelbase.dll). Diese Implementierung des IUM verwendet zum größten Teil dieselben Standardbibliotheken der Win32-API, was den Speicheroverhead von VTL-1-Benutzermodusanwendungen zu reduzieren hilft, da im Grunde genommen derselbe Benutzermoduscode vorhanden ist wie in ihren VTL-0-Gegenstücken. Mechanismen für das Kopieren bei Schreibvorgängen (siehe Kapitel 5) verhindern, dass VTL-0-Anwendungen Änderungen an Binärdateien vornehmen, die auf VTL 1 genutzt werden. Bei VBS gelten die üblichen Regeln für Benutzer- und Kernelmodus, die jetzt aber noch um VTL-Aspekte erweitert werden. Kernelmoduscode auf VTL 0 darf keinen Benutzermoduscode auf VTL 1 anfassen, da VTL 1 höhere Rechte hat. Allerdings darf Benutzermoduscode auf VTL 1 auch keinen Kernelmoduscode auf VTL 0 anfassen, da ein solcher Zugriff vom Benutzermodus (Ring 3) auf den Kernelmodus (Ring 0) grundsätzlich untersagt ist. VTL-1-Benutzermodusanwendungen müssen außerdem weiterhin die regulären Windows-Systemaufrufe und die zugehörigen Zugriffsprüfungen durchlaufen, um auf Ressourcen zuzugreifen. Das können Sie sich auf folgende Weise verdeutlichen: Rechteebenen (Benutzer- oder Kernelmodus) sorgen für Macht, VTLs dagegen für Isolierung. Eine VTL-1-Benutzermodusanwendung ist nicht mächtiger als eine VTL-0-Anwendung oder ein VTL-0-Treiber, aber davon Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick Kapitel 2 67
isoliert. In vielen Fällen sind VTL-1-Anwendungen nicht nur nicht mächtiger, sondern sogar weniger mächtig. Da der sichere Kernel nicht die volle Palette der Systemfähigkeiten implementiert, wählt er aus, welche Systemaufrufe er an den VTL-0-Kernel weiterleitet (weshalb der sichere Kernel auch als Proxykernel bezeichnet wird). Jede Art von E/A, ob im Zusammenhang mit Dateien, dem Netzwerk oder der Registrierung, ist komplett verboten. Auch Grafik kommt nicht in Betracht, und es darf auch mit keinem einzigen Treiber kommuniziert werden. Da der sichere Kernel jedoch auf VTL 1 und im Kernelmodus läuft, hat er vollständigen Zugriff auf den Arbeitsspeicher und die Ressourcen auf VTL 0. Er kann den Hypervisor nutzen, um den VTL-0-Betriebssystemzugriff auf bestimmte Speicherbereiche einzuschränken. Dies geschieht mithilfe von SLAT (Second Level Address Translation), einer CPU-Hardwaretechnologie. Sie bildet die Grundlage von CredentialGuard, mit dem Geheimnisse in solchen Bereichen gespeichert werden. Der sichere Kernel kann SLAT auch nutzen, um die Ausführung von Speicherbereichen zu untersagen und zu steuern, was eine entscheidende Bedingung von DeviceGuard ist. Um zu verhindern, dass normale Gerätetreiber Hardwaregeräte für den direkten Speicherzugriff nutzen, verwendet das System noch eine weitere Hardware, nämlich die E/A-Speicherverwaltungseinheit (Memory Management Unit, MMU), die den Speicherzugriff für Geräte im Grunde genommen virtualisiert. Damit kann verhindert werden, dass Gerätetreiber den direkten Speicherzugriff (Direct Memory Access, DMA) nutzen, um unmittelbar auf die physischen Speicherbereiche des Hypervisors oder des sicheren Kernels zuzugreifen. Mit einer solchen Vorgehensweise könnte SLAT umgangen werden, da dabei kein virtueller Speicher verwendet wird. Da der Hypervisor die erste Systemkomponente ist, die der Bootloader lädt, kann er die SLAT und die I/O MMU nach seinem Bedarf programmieren und die Ausführungsumgebungen VTL 0 und VTL 1 definieren. Dann wird der Bootloader in VTL 1 erneut ausgeführt, um den sicheren Kernel zu laden, der das System nach seinen Bedürfnissen weiter konfiguriert. Erst danach wird die VTL gesenkt, sodass der normale Kernel in seinem VTL-0-Gefängnis ausgeführt wird, aus dem er nicht entkommen kann. Da Benutzermodusprozesse auf VTL 1 isoliert sind, kann potenziell schädlicher Code zwar keinen größeren Einfluss auf das System gewinnen, aber heimlich versuchen, sichere Systemaufrufe durchzuführen (die ihm erlauben würden, seine eigenen Geheimnisse zu signieren), und möglicherweise schädliche Interaktionen mit anderen VTL-1-Prozessen oder dem sicheren Kernel hervorrufen. Daher dürfen nur besonders signierte Binärdateien, sogenannte Trustlets, auf VTL 1 ausgeführt werden. Jedes Trustlet weist einen eindeutigen Bezeichner und eine Signatur auf, und der sichere Kernel verfügt über hartcodierte Kenntnisse darüber, welche Trustlets bisher erstellt wurden. Daher ist es unmöglich, neue Trustlets zu erstellen, ohne auf den sicheren Kernel zuzugreifen (den nur Microsoft anfassen kann). Auch vorhandene Trustlets können nicht manipuliert werden, da dadurch ihre besondere Microsoft-Signatur verloren ginge. Weitere Informationen über Trustlets erhalten Sie in Kapitel 3. Die Ergänzung um den sicheren Kernel und VBS ist eine spannende Entwicklung in der modernen Betriebssystemarchitektur. Mit weiteren Hardwareänderungen an Bussen wie PCI und USB wird es bald möglich sein, eine ganze Klasse von sicheren Geräten zu unterstützen, die in Kombination mit einer minimalistischen sicheren HAL, einem sicheren Plug-&-Play-Manager und einem sicheren Benutzermodus-Geräteframework bestimmten VTL-1-Anwendungen ei- 68 Kapitel 2 Systemarchitektur
nen direkten und gesonderten Zugriff auf eigens dafür vorgesehene Dienste erlauben können, z. B. für biometrische oder Smartcard-Eingaben. Neue Versionen von Windows 10 werden sehr wahrscheinlich solche technischen Fortschritte nutzen. Hauptsystemkomponenten Nachdem wir uns einen Überblick über die Architektur von Windows verschafft haben, wollen wir uns nun eingehender mit der inneren Struktur und mit der Rolle der einzelnen Komponenten befassen. Abb. 2–4 zeigt eine detaillierte und vollständigere Ansicht der Kernarchitektur und der Komponenten von Windows als Abb. 2–1. Beachten Sie aber, dass dies immer noch nicht alle Komponenten sind. (Insbesondere fehlen die Netzwerkkomponenten, die in Kapitel 10, »Netzwerk«, von Band 2 behandelt werden.) System­ prozesse Umgebungs­ teilsysteme Dienste Anwendungen Wininit SitzungsManager Spooler SvcHost Benutzer­ prozess Teilsystem-DLLs Teilsystem-DLLs Winlogon SCM CSRSS Teilsystem-DLLs NTDLL.DLL Benutzermodus Kernelmodus Systemdienst-Dispatcher ALPC Konfigura­ tions-Manager ObjektManager ProzessManager SpeicherManager Plug-&-PlayManager EnergieManager Geräte- und Dateisystemtreiber Sicherheits­ verweis­ monitor E/AManager Windows USER und GDI Kernel Hardwareabstraktionsschicht (HAL) Grafik­ treiber HAL-Erweiterungen Hypervisor Hyper-V Abbildung 2–4 Architektur von Windows Die folgenden Abschnitte enthalten ausführliche Beschreibungen der wichtigsten Elemente in diesem Diagramm. Kapitel 8 in Band 2 erläutert die primären Steuermechanismen, die das System einsetzt (wie den Objekt-Manager, Interrupts usw.), Kapitel 11, »Starten und Herunterfahren«, beschreibt den Vorgang des Startens und Herunterfahrens und Kapitel 9 gibt ausführliche Informationen über Verwaltungsmechanismen wie die Registrierung, Dienstprozesse und WMI. In anderen Kapiteln werden noch genauere Erklärungen zu den internen Strukturen und der Funktionsweise von Schlüsselaspekten wie Prozessen und Threads, Speicherverwaltung, Sicherheit, dem E/A-Manager, der Speicherplatzverwaltung, dem Cache-Manager, dem Windows-Dateisystem (NTFS) und dem Netzwerk gemacht. Hauptsystemkomponenten Kapitel 2 69
Umgebungsteilsysteme und Teilsystem-DLLs Ein Umgebungsteilsystem hat die Aufgabe, Anwendungsprogrammen einen Teil der grundlegenden Dienste bereitzustellen, die es im Exekutivsystem von Windows gibt. Jedes Teilsystem bietet dabei Zugriff auf eine andere Auswahl der nativen Windows-Dienste. Das bedeutet, dass eine Anwendung in einem Teilsystem Dinge tun kann, die einer Anwendung in einem anderen Teilsystem nicht möglich sind. Beispielsweise kann eine Windows-Anwendung die SUA-Funk­ tion fork nicht nutzen. Jedes ausführbare Abbild (.exe) ist an genau ein Teilsystem gebunden. Wird das Abbild ausgeführt, so untersucht der Code zur Prozesserstellung den Typcode für das Teilsystem im Abbild-Header, sodass er das entsprechende Teilsystem über den neuen Prozess informieren kann. Der Typcode wird mit der Option /SUBSYSTEM des Linkers von Microsoft Visual Studio angegeben (oder im Eintrag SubSystem auf der Seite Linker/System in den Projekteigenschaften). Wie bereits erwähnt, rufen Benutzeranwendungen die Windows-Systemdienste nicht direkt auf, sondern durchlaufen dazu eine oder mehrere Teilsystem-DLLs. Diese Bibliotheken exportieren die dokumentierte Schnittstelle, die die mit dem Teilsystem verlinkten Programme aufrufen können. Beispielsweise implementieren die DLLs des Windows-Teilsystems (wie Kernel32.dll, Advapi32.dll, User32.dll und Gdi32.dll) die Funktionen der Windows-API, während die DLL des SUA-Teilsystems (Psxdll.dll) zur Implementierung der SUA-API-Funktionen dient (auf Windows-Versionen, die POSIX unterstützen). Experiment: Den Teilsystemtyp des Abbilds einsehen Den Teilsystemtyp des Abbilds können Sie sich mit dem Werkzeug Dependency Walker (Depends.exe) ansehen. In den folgenden Abbildungen sehen Sie z. B. die Typen für die beiden Windows-Abbilder Notepad.exe (der einfache Windows-Texteditor) und Cmd.exe (die Windows-Eingabeaufforderung): → 70 Kapitel 2 Systemarchitektur
Dies zeigt, dass es sich bei Notepad um ein GUI-Programm handelt, bei Cmd dagegen um ein Konsolenprogramm. Das scheint zu bedeuten, dass es zwei verschiedene Teilsysteme für grafische und für textgestützte Programme gibt, allerdings gibt es in Wirklichkeit nur ein Windows-Teilsystem. GUI-Programme können auch Konsolen haben (durch Aufrufen der Funktion AllocConsole), und genauso gut können Konsolenprogramme grafische Benutzeroberflächen aufrufen. Wenn eine Anwendung eine Funktion in einer Teilsystem-DLL aufruft, können drei verschiedene Situationen vorliegen: QQ Die Funktion ist vollständig im Benutzermodus innerhalb der DLL implementiert. Das heißt, es werden keine Nachrichten an den Prozess des Umgebungsteilsystems gesendet und keine Dienste des Windows-Exekutivsystems aufgerufen. Die Funktion läuft im Benutzermodus ab, und die Ergebnisse werden an den Aufrufer zurückgegeben. Beispiele für solche Funktionen sind GetCurrentProcess (die immer -1 zurückgibt; dieser Wert ist so definiert, dass er in allen prozessbezogenen Funktionen auf den aktuellen Prozess verweist) und GetCurrentProcessId. (Da sich die Prozesskennung für einen laufenden Prozess nicht ändert, wird sie von einem zwischengespeicherten Speicherort abgerufen, um einen Aufruf im Kernel zu vermeiden.) QQ Die Funktion erfordert einen oder mehrere Aufrufe der Windows-Exekutive. Beispielsweise müssen die Funktionen ReadFile und WriteFile die zugrunde liegende (und für die Verwendung im Benutzermodus nicht dokumentierten) Dienste NtReadFile bzw. NtWriteFile des E/A-Systems von Windows aufrufen. QQ Für die Funktion muss einige Arbeit im Prozess des Umgebungsteilsystems erledigt werden. (Diese Prozesse laufen im Benutzermodus und sind dafür verantwortlich, den Status der Clientanwendungen, die unter ihrer Kontrolle ausgeführt werden, zu erhalten.) In diesem Fall wird eine Client/Server-Anforderung an das Umgebungsteilsystem gestellt. Dazu wird eine ALPAC-Nachricht (siehe Kapitel 8 in Band 2) an das Teilsystem gesendet, um eine Operation auszuführen. Die Teilsystem-DLL wartet dann auf eine Antwort, bevor sie die Steuerung an den Aufrufer zurückgibt. Bei einigen Funktionen kann eine Kombination des zweiten und des dritten Falls vorliegen, etwa bei den Windows-Funktionen CreateProcess und ExitWindowsEx. Start von Teilsystemen Teilsysteme werden vom Prozess Smss.exe, dem Sitzungs-Manager, gestartet. Die Informationen über den Start von Teilsystemen werden unter dem Registrierungsschlüssel HKLM\SYSTEM\ CurrentControlSet\Control\Session Manager\SubSystems gespeichert. Die Werte unter diesem Schlüssel sind in Abb. 2–5 dargestellt (Screenshot von Windows 10 Pro). Hauptsystemkomponenten Kapitel 2 71
Abbildung 2–5 Registrierungs-Editor mit Informationen über Windows-Teilsysteme Der Wert Required führt die Teilsysteme auf, die beim Start des Systems geladen werden. In diesem Wert stehen die beiden Strings Windows und Debug. Der Wert Windows enthält die Dateispezifikation des Windows-Teilsystems Csrss.exe, was für Client/Server Runtime Subsystem steht. Debug dagegen ist leer (dieser Wert wird seit Windows XP nicht mehr benötigt, wird aber aus Gründen der Kompatibilität beibehalten) und tut daher gar nichts. Der Wert Optional gibt optionale Teilsysteme an und ist hier auch leer, da SUA auf Windows 10 nicht mehr zur Verfügung steht. Wäre das noch der Fall, würde der POSIX-Datenwert auf einen anderen Wert zeigen, der wiederum auf Psxss.exe verweist (den Prozess für das POSIX-Teilsystem). Da dieser Wert in Optional stehen würde, so würde er »bei Bedarf geladen«, also bei der ersten Begegnung mit einem POSIX-Abbild. Der Registrierungswert Kmode enthält den Namen der Datei für den Kernelmodusteil des Windows-Teilsystems, Win32k.sys (die weiter hinten in diesem Kapitel noch erklärt wird). Sehen wir uns nun die Windows-Umgebungsteilsysteme genauer an. Windows-Teilsystem Windows wurde zwar dafür ausgelegt, mehrere unabhängige Umgebungsteilsysteme zu unterstützen, doch wenn jedes Teilsystem den gesamten Code zur Handhabung der Fenster und der Anzeige-E/A implementierte, würde das zu einer erheblichen Duplizierung von Systemfunktionen führen und letzten Endes sowohl die Größe als auch die Leistung des Systems beeinträchtigen. Da Windows das primäre Teilsystem sein sollte, brachten die Designer diese Grundfunktionen dort unter, sodass andere Teilsysteme, etwa wie SUA, Dienste des Windows-Teilsystems aufrufen mussten, um eine Anzeige-E/A durchzuführen. Aufgrund dieser Designentscheidung ist das Windows-Teilsystem eine erforderliche Komponente für alle Windows-Systeme, selbst für Serversysteme, auf denen keine Benutzer für interaktive Sitzungen angemeldet sind. Der Prozess ist daher als kritischer Prozess gekennzeichnet, d. h., sollte er aus irgendeinem Grund beendet werden, so stürzt das System ab. 72 Kapitel 2 Systemarchitektur
Das Windows-Teilsystem besteht aus den folgenden Hauptkomponenten: QQ Für jede Sitzung lädt eine Instanz des Umgebungsteilsystemprozesses (Csrss.exe) vier DLLs (Basesrv.dll, Winsrv.dll, Sxssrv.dll und Csrsrv.dll), die Unterstützung für Folgendes bieten: • Verschiedene Ordnungsaufgaben im Zusammenhang mit dem Erstellen und Löschen von Prozessen und Threads • Herunterfahren von Windows-Anwendungen (durch die API ExitWindowsEx) • Bereitstellen von Zuordnungen von .ini-Dateien zu Speicherorten in der Registrierung, um für Rückwärtskompatibilität zu sorgen • Senden bestimmter Kernelbenachrichtigungen (z. B. derjenigen aus dem Plug-&-PlayManager) als Windows-Nachrichten (WL_DEVICECHANGE) an Windows-Anwendungen • Teile der Unterstützung für 16-Bit-VDM-Prozesse (Virtual DOS Machine; nur auf 32-BitWindows) • Unterstützung für Side-by-Side (SxS)/Fusion und Manifestcaches • Verschiedene Unterstützungsfunktionen für natürliche Sprache, um eine Zwischenspeicherung zu ermöglichen Der Kernelmoduscode für den Roheingabethread und den Desktopthread (verantwortlich für den Mauscursor, Tastatureingaben und den Umgang mit dem Desktop-Fenster) wird innerhalb der Threads ausgeführt, die in Winsrv.dll laufen. Daneben enthalten die Csrss.exe-Instanzen im Zusammenhang mit interaktiven Benutzersitzungen eine fünfte DLL namens Cdd.dll (Canonical Display Driver). Sie ist für die Kommunikation mit der DirectX-Unterstützung im Kernel (siehe nachfolgende Erklärung) bei jeder vertikalen Aktualisierung (VSync) zuständig, um den sichtbaren Desktop ohne herkömmliche hardwarebeschleunigte GDI-Unterstützung zu zeichnen. QQ Ein Kernelmodus-Gerätetreiber (Win32k.sys) mit folgendem Inhalt: • Der Fenster-Manager, der die Fensteranzeige steuert, die Bildschirmausgabe verwaltet, Eingaben von der Tastatur, der Maus und anderen Geräten erfasst und Benutzernachrichten an Anwendungen übergibt • Die Grafikgerätschnittstelle (Graphics Device Interface, GDI), bei der es sich um eine Bibliothek von Funktionen für grafische Ausgabegeräte handelt und die Funktionen für das Zeichnen von Linien, Text und Formen sowie zur Bearbeitung von Grafiken enthält • Wrapper für die DirectX-Unterstützung, die in einem anderen Kerneltreiber implementiert ist (Dxgkrnl.sys) QQ Der Konsolenhostprozess (Conhost.exe), der Unterstützung für Konsolenanwendung bietet QQ Der Desktop Window Manager (Dwm.exe), der es ermöglicht, die Darstellung der sichtbaren Fenster mithilfe von CDD und DirectX zu einer einzigen Oberfläche zusammenzufassen QQ Teilsystem-DLLs (wie Kernel32.dll, Advapi32.dll, User32.dll und Gdi32.dll), die dokumentierte Windows-API-Funktionen in die geeigneten und (für den Benutzermodus) nicht dokumentierten Kernelmodus-Systemdienstaufrufe in Ntoskrnl.exe und Win32k.sys übersetzen Hauptsystemkomponenten Kapitel 2 73
QQ Grafikgerätetreiber für hardwareabhängige Grafikanzeigetreiber, Druckertreiber und Video-Miniporttreiber Nach einem Refactoring der Windows-Architektur unter der Bezeichnung MinWin bestehen die Teilsystem-DLLs jetzt generell aus spezifischen Bibliotheken, die APISets implementieren, zu der Teilsystem-DLL verknüpft und mithilfe eines besonderen Umleitungsverfahrens aufgelöst werden. Weitere Informationen über diesen Refactoringvorgang erhalten Sie im Abschnitt »Der Abbildlader« in Kapitel 3. Win32k.sys in Windows 10 Die grundlegenden Anforderungen an die Fensterverwaltung in Windows 10 können je nach vorliegendem Gerät erheblich schwanken. Beispielsweise braucht ein voll ausgestatteter Desktopcomputer alle Möglichkeiten des Fenster-Managers wie veränderliche Fenstergrößen, Besitzerfenster, Kindfenster usw. Windows Mobile 10 auf Smartphones und kleinen Tablets dagegen braucht viele dieser Merkmale nicht, da immer nur ein Fenster im Vordergrund steht und nicht minimiert, vergrößert, verkleinert oder auf ähnliche Weise verändert werden kann. Das Gleiche gilt auch für IoT-Geräte, von denen viele nicht einmal eine Anzeige haben. Aus diesem Grunde wurde der Funktionsumfang von Win32k.sys auf verschiedene Kernelmodule aufgeteilt, wobei auf einem System nicht unbedingt alle diese Module gebraucht werden. Das reduziert die Angriffsfläche des Fenster-Managers erheblich, da der Code an Komplexität verliert und viele seiner älteren Teile entfernt wurden. Betrachten Sie dazu die folgenden Beispiele: QQ Auf Smartphones (Windows Mobile 10) lädt Win32k.sys nur Win32kMin.sys und Win32kBase.sys. QQ Auf Desktopsystemen lädt Win32k.sys nur Win32kBase.sys und Win32kFull.sys. QQ Auf bestimmten IoT-Geräten braucht Win32k.sys lediglich Win32kBase.sys. Anwendungen rufen die Standardfunktion USER auf, um Steuerelemente der Benutzerschnittstelle wie Fenster und Schaltflächen auf der Anzeige zu erstellen. Der Fenster-Manager vermittelt diese Anforderungen an die GDI, die sie wiederum an die Grafikgerätetreiber übergibt, wo sie für das Anzeigegerät formatiert werden. Um die Unterstützung für die Videoanzeige zu vervollständigen, wird ein Anzeigetreiber mit einem Video-Miniporttreiber gekoppelt. Die GDI stellt einen Satz von 2-D-Standardfunktionen bereit, mit denen Anwendungen mit Grafikgeräten kommunizieren können, ohne etwas über sie zu wissen. Die GDI-Funktionen vermitteln zwischen Anwendungen und Grafikgeräten wie Anzeige- und Druckertreibern. Die GDI interpretiert die Anforderungen der Anwendung für Grafikausgabe und sendet die Anforderungen an die Grafikgerätetreiber. Außerdem stellt sie eine Standardschnittstelle bereit, über die die Anwendungen verschiedene Grafikausgabegeräte verwenden können. Diese Schnittstelle macht den Anwendungscode von den Hardwaregeräten und deren Treibern unabhängig. Die GDI schneidet ihre Nachrichten auf die Fähigkeiten der Geräte zu, wobei sie eine Anforderung oft in handhabbare Teile zerlegt. Beispielsweise verstehen manche Geräte die Anweisung, eine Ellipse zu zeichnen, während die GDI bei anderen den Befehl als eine Folge von Pixeln 74 Kapitel 2 Systemarchitektur
deuten muss, die an bestimmten Koordinaten zu platzieren sind. Weitere Informationen über die Architektur von Grafik- und Videotreibern finden Sie im Abschnitt »Design Guide« des Kapitels »Display (Adapters and Monitors)« des WDK. Da ein Großteil des Teilsystems – insbesondere die Funktionen zur Anzeige-E/A – im Kernelmodus läuft, führen nur wenige Windows-Funktionen dazu, dass eine Nachricht an den Windows-Teilsystemprozess gesendet wird, nämlich die Erstellung und Beendigung von Prozessen und Threads und die Zuordnung von DOS-Laufwerksbuchstaben (etwa durch subst.exe). Im Allgemeinen veranlasst eine laufende Windows-Anwendung wenn überhaupt nur wenige Kontextwechsel zum Windows-Teilsystemprozess. Ausnahmen sind lediglich die erforderlichen Kontextwechsel, um die neue Position des Mauscursors zu zeichnen, die Tastatureingaben zu verarbeiten und den Bildschirm über CDD wiederzugeben. Konsolenfensterhost Im ursprünglichen Design des Windows-Teilsystems war der Teilsystemprozess (Csrss.exe) für die Verwaltung des Konsolenfensters verantwortlich, und jede Konsolenanwendung (wie Cmd.exe, die Eingabeaufforderung) kommunizierte mit ihm. Ab Windows 7 wurde für jedes Konsolenfenster im System ein eigener Prozess verwendet, nämlich der Konsolenfensterhost (Conhost.exe). (Ein Konsolenfenster kann von mehreren Konsolenanwendungen gemeinsam genutzt werden. Wenn Sie beispielsweise eine Eingabeaufforderung von einer anderen Eingabeaufforderung aus aufrufen, so verwendet die zweite Eingabeaufforderung das Konsolenfenster der ersten.) Die Einzelheiten des Konsolenhosts von Windows 7 wurden in Kapitel 2 der sechsten Ausgabe dieses Buches erklärt. Mit Windows 8 wurde die Konsolenarchitektur erneut geändert. Den Prozess Conhost. exe gibt es nach wie vor, aber er wird jetzt vom Konsolentreiber (\Windows\System32\Drivers\ConDrv.sys) von dem konsolengestützten Prozess aus ausgelöst (also nicht mehr von Csrss.exe aus wie in Windows 7). Der betreffende Prozess kommuniziert mit Conhost.exe über den Konsolentreiber (ConDrv.sys), indem er Lese-, Schreib-, E/A-Steuerungs- und andere Arten von E/A-Anforderungen sendet. Conhost.exe fungiert als Server und der Prozess, der die Konsole nutzt, als Client. Dank dieser Änderung ist es nicht mehr nötig, dass Csrss. exe Tastatureingaben (im Rahmen des Roheingabethreads) empfängt, durch Win32k.sys an Conhost.exe sendet und dann mit ALPC an Cmd.exe schickt. Stattdessen kann die Anwendung für die Eingabeaufforderung Eingaben über Lese/-Schreib-Eingaben und -Ausgaben direkt vom Konsolentreiber empfangen, wodurch überflüssige Kontextwechsel verhindert werden. Der folgende Screenshot aus Process Explorer zeigt das Handle, den Conhost.exe für das von ConDrv.sys verfügbar gemachte Geräteobjekt \Device\ConDrv geöffnet hält. (Weitere Einzelheiten zu Gerätenamen und E/A erhalten Sie in Kapitel 6.) → Hauptsystemkomponenten Kapitel 2 75
Conhost.exe ist ein Kindprozess des Konsolenprozesses (hier von Cmd.exe). Ausgelöst wird die Erzeugung von Conhost durch den Abbildlader für das Konsolenteilsystemabbild oder auf Anforderung, wenn ein GUI-Teilsystemabbild die Windows-API AllocConsole aufruft. (Natürlich sind das GUI- und das Konsolenteilsystem in dem Sinne identisch, dass beide Varianten des Windows-Teilsystemtyps darstellen.) Die Hauptarbeit übernimmt die DLL, die Conhost lädt (\Windows\SYstem32\ConhostV2.dll). Sie enthält den Großteil des Codes, der mit dem Konsolentreiber kommuniziert. Weitere Teilsysteme Wie bereits erwähnt, unterstützte Windows ursprünglich POSIX- und OS/2-Teilsysteme. Da sie jedoch nicht mehr im Lieferumfang von Windows enthalten sind, behandeln wir sie in diesem Buch auch nicht. Das allgemeine Prinzip der Teilsysteme gibt es jedoch nach wie vor, sodass das System durch neue Teilsysteme erweitert werden kann, sollte in Zukunft Bedarf dafür aufkommen. Pico-Anbieter und das Windows-Teilsystem für Linux Das herkömmliche Teilsystemmodell ist zwar erweiterbar und war offensichtlich leistungsfähig genug, um POSIX und OS/2 ein Jahrzehnt lang zu unterstützen, weist aber zwei erhebliche technische Nachteile auf, weshalb Nicht-Windows-Binärdateien keine breite Anwendung fanden, sondern nur in einigen Sonderfällen genutzt wurden: QQ Da die Teilsysteminformationen aus dem PE-Header (Portable Executable) bezogen werden, muss der Quellcode der ursprünglichen Binärdatei das Teilsystem als ausführbare Windows-PE-Datei (.exe) neu erstellen. Dadurch werden auch jegliche POSIX-Abhängigkeiten und -Systemaufrufe in Windows-Importe der Bibliothek Psxdll.dll geändert. QQ Das Teilsystem ist durch den Funktionsumfang des Win32-Teilsystems (auf dem es manchmal aufsitzt) oder des NT-Kernels eingeschränkt. Daher emuliert es nicht das von der POSIX-Anwendung angeforderte Verhalten, sondern bildet nur einen Wrapper dafür. Das kann manchmal zu leichten Kompatibilitätsstörungen führen. 76 Kapitel 2 Systemarchitektur
Außerdem war das POSIX-Teilsystem/SUA, wie der Name schon sagt, für POSIX/UNIX-Anwendungen ausgelegt, die vor Jahrzehnten den Servermarkt beherrschten, und nicht für die heute gängigen Linux-Anwendungen. Um diese Probleme zu lösen, was eine andere Herangehensweise an den Aufbau eines Teilsystems erforderlich – eine, die keinen herkömmlicher Benutzermodus-Wrapper für die Systemaufrufe der Umgebung und die Ausführung herkömmlicher PE-Abbilder erforderte. Das Projekt Drawbridge von Microsoft Research erwies sich als das ideale Mittel für einen neuen Zugang zum Thema Teilsysteme. Dies führte zur Umsetzung des Pico-Modells. Bei diesem Modell werden Pico-Anbieter verwendet. Dabei handelt es sich um benutzerdefinierte Kernelmodustreiber, die über die API PsRegisterPicoProvider Zugriff auf spezialisierte Kernelschnittstellen erhalten. Diese spezialisierten Schnittstellen bieten die folgenden beiden Vorteile: QQ Sie ermöglichen dem Anbieter, Pico-Prozesse und -Threads zu erstellen und deren Ausführungskontexte, Segmente und Speicherdaten in den zugehörigen EPROCESS- bzw. ETHREAD-Strukturen anzupassen (mehr darüber in Kapitel 3 und 4). QQ Sie ermöglichen dem Anbieter, eine breite Palette von Benachrichtigungen zu empfangen, wenn diese Prozesse oder Threads an bestimmten Systemaktivitäten wie Systemaufrufen, Ausnahmen, APCs, Seitenfehlern, Terminierungen, Kontextwechseln, Anhalten, Wiederaufnahmen usw. beteiligt sind. Ab Windows 10 Version 1607 ist ein solcher Pico-Anbieter vorhanden, nämlich Lxss.sys mit seinem Partner Lxcore.sys. Diese Treiber bilden die Pico-Anbieterschnittstelle für die Komponente WSL (Windows Subsystem for Linux). Der Pico-Anbieter empfängt fast alle möglichen Übergänge zwischen Benutzer- und Kernelmodus (z. B. Systemaufrufe und Ausnahmen). Sofern die darunter laufenden Pico-Prozesse über einen Adressraum verfügen, den der Anbieter erkennen kann, über Code verfügt, der nativ darin ausgeführt werden kann, und die Übergänge komplett transparent erfolgen, spielt der darunter liegende »echte« Kernel keine große Rolle. Wie Sie in Kapitel 3 noch sehen werden, unterscheiden sich Pico-Prozesse, die unter dem WSL-Pico-Anbieter ausgeführt werden, erheblich von normalen Windows-Prozessen. Beispielsweise fehlt ihnen die Ntdll.dll, die in jeden normalen Prozess geladen wird. Stattdessen enthält ihr Arbeitsspeicher Strukturen wie ein vDSO, ein besonderes Abbild, das es nur auf Linux/BSD-Systemen gibt. Wenn die Linux-Prozesse transparent laufen sollen, müssen sie ausgeführt werden können, ohne eine Neukompilierung als ausführbare PE-Windows-Dateien zu erfordern. Da der Windows-Kernel nicht weiß, wie er andere Abbildtypen zuordnen soll, können solche Abbilder nicht von Windows-Prozessen über die API CreateProcess gestartet werden und solche APIs auch nicht selbst aufrufen (denn schließlich wissen sie nicht einmal, dass sie auf Windows laufen). Die Unterstützung für eine solche Art von Interoperabilität bieten der Pico-Anbieter und der LXSS-Manager, ein Dienst des Benutzermodus. Ersterer implementiert eine private Schnittstelle zur Kommunikation mit dem LXSS-Manager, Letzterer eine COM-Schnittstelle zur Kommunikation mit dem besonderen Startprozess Bash.exe und dem Verwaltungsprozess Lxrun. exe. Das folgende Diagramm gibt einen Überblick über die Komponenten von WSL. Hauptsystemkomponenten Kapitel 2 77
Linux-Instanz (Pico-Prozess) Bash.exe LXSS-MangerDienst /bin/bash Benutzermodus LXCore / LXSS Kernelmodus Exekutive und Kernel Unterstützung für eine breite Palette von Linux-Anwendungen zu geben, ist ein erhebliches Unterfangen. Linux umfasst Hunderte von Systemaufrufen, ungefähr so viele wie der Windows-Kernel selbst. Der Pico-Anbieter kann zwar vorhandene Windows-Merkmale nutzen – von denen viele zur Unterstützung des ursprünglichen POSIX-Teilsystems erstellt wurden, z. B. für fork() –, doch manche Funktionalität muss er selbst neu implementieren. Obwohl beispielsweise NTFS zur Speicherung des tatsächlichen Dateisystems genutzt wird (und nicht EXTFS), verfügt der Pico-Anbieter über eine komplette Implementierung des Linux-VFS (Virtual File System) einschließlich Unterstützung von Inodes, inotify() sowie /sys, /dev und ähnlichen Linux-Dateisystem-Namensräumen mit dem entsprechenden Verhalten. Ebenso kann der Pico-Anbieter zwar WSK (Windows Sockets for Kernel) für die Netzwerkanbindung nutzen, verfügt über komplexe Wrapper um das eigentliche Socketverhalten, sodass er UNIX-Domänensockets, Linux-NetLink-Sockets und Internet-Standardsockets unterstützen kann. Manche vorhandenen Windows-Einrichtungen sind auch einfach nicht ausreichend kompatibel, wobei die Abweichungen ganz gering sein können. Beispielsweise verfügt Windows über einen Treiber für benannte Pipes (Npfs.sys), der den herkömmlichen IPC-Pipe-Mechanismus unterstützt. Er unterscheidet sich jedoch nur ein wenig, aber doch ausreichend stark von Linux-Pipes, um eine Anwendung funktionsunfähig zu machen. Das erfordert eine komplette Neuimplementierung von Pipes für Linux-Anwendungen ohne Zuhilfenahme des Kerneltreibers Npfs.sys. Da sich dieses Merkmal zurzeit noch im Beta-Stadium befindet und erheblichen Änderungen unterliegen kann, behandeln wir die internen Mechanismen dieses Teilsystems in diesem Buch nicht. Allerdings werfen wir in Kapitel 3 noch einen weiteren Blick auf Pico-Prozesse. Wenn das Teilsystem ausgereift ist, wird es wahrscheinlich eine offizielle Dokumentation auf MSDN und stabile APIs für die Interaktion mit Linux-Prozessen geben. Ntdll.dll Ntdll.dll ist eine besondere Systemunterstützungsbibliothek, die hauptsächlich für die Verwendung durch Teilsystem-DLLs und native Anwendung da ist. (Nativ bezeichnet in diesem Zusammenhang Abbilder, die nicht an ein bestimmtes Teilsystem gebunden sind.) Sie enthält Funktionen der beiden folgenden Arten: 78 Kapitel 2 Systemarchitektur
QQ Systemdienst-Dispatchstubs zu Systemdiensten der Windows-Exekutive QQ Interne Unterstützungsfunktionen, die von Teilsystemen, Teilsystem-DLLs und anderen nativen Abbilder verwendet werden Die erste Gruppe dieser Funktionen bildet eine Schnittstelle zu den Systemdiensten der Windows-Exekutive, die vom Benutzermodus aus aufgerufen werden können. Es gibt über 450 solcher Funktionen, z. B. NtCreateFile und NtSetEvent. Die meisten Möglichkeiten, die diese Funktionen bieten, sind über die Windows-API zugänglich. (Einige jedoch nicht; sie sind nur zur Verwendung durch bestimmte interne Komponenten des Betriebssystems gedacht.) Für jede dieser Funktionen enthält Ntdll.dll einen Eintrittspunkt mit demselben Namen. Der Code in der Funktion enthält die architekturspezifische Anweisung für den Übergang in den Kernelmodus, um den Systemdienst-Dispatcher aufzurufen. (Dies wird ausführlicher in Kapitel 8 von Band 2 erläutert.) Nach der Überprüfung einiger Parameter ruft der Systemdienst-Dispatcher den Kernelmodus-Systemdienst auf, der den eigentlichen Code innerhalb von Ntoskrnl.exe enthält. Das folgende Experiment zeigt, wie diese Funktionen aussehen. Experiment: Den Systemdienst-Dispatchercode anzeigen Öffnen Sie die Version von WinDbg für Ihre Systemarchitektur (also etwa die x64-Version auf 64-Bit-Windows). Wählen Sie dann im Menü File den Befehl Open Executable. Wechseln Sie zu %SystemRoot%\System32 und markieren Sie Notepad.exe. Daraufhin wird der Editor (Notepad) gestartet. Der Debugger hält am ersten Haltepunkt an. Das geschieht schon sehr früh im Lauf des Prozesses, wovon Sie sich überzeugen können, indem Sie den Befehl k (für den Aufrufstack) ausführen. Dort sehen Sie einige Funktionen, deren Namen mit Ldr beginnen, was auf den Abbildlader hindeutet. Die Hauptfunktion des Editors ist noch nicht ausgeführt worden, weshalb sein Fenster nicht angezeigt wird. Setzen Sie wie folgt einen Haltepunkt in NtCreateFile innerhalb von Ntdll.dll (der Debugger unterscheidet nicht zwischen Groß- und Kleinschreibung): bp ntdll!ntcreatefile Geben Sie den Befehl g (»go«) ein oder drücken Sie (F5), um die Ausführung des Editors fortzusetzen. Der Debugger sollte sofort wieder anhalten und dabei etwas wie das Folgende (auf x64) ausgeben: Breakpoint 0 hit ntdll!NtCreateFile: 00007ffa'9f4e5b10 4c8bd1 mov r10,rcx Möglicherweise wird der Funktionsname als ZwCreateFile angezeigt. Sowohl ZwCreateFile als auch NtCreateFile verweisen auf dasselbe Symbol im Benutzermodus. Geben Sie nun den Befehl u ein (»unassembled«), um einige Anweisungen weiter nach vorn zu schauen: 00007ffa'9f4e5b10 4c8bd1 mov r10,rcx 00007ffa'9f4e5b13 b855000000 mov eax,55h → Hauptsystemkomponenten Kapitel 2 79
00007ffa'9f4e5b18 f604250803fe7f01 test byte ptr [SharedUserData+0x308 jne ntdll!NtCreateFile+0x15 (00000000'7ffe0308)],1 00007ffa'9f4e5b20 7503 (00007ffa'9f4e5b25) 00007ffa'9f4e5b22 0f05 syscall 00007ffa'9f4e5b24 c3 ret 00007ffa'9f4e5b25 cd2e int 00007ffa'9f4e5b27 c3 ret 2Eh Das Register EAX wird auf die Nummer gesetzt, die der Systemdienst in dem vorliegenden Betriebssystem hat (hier bei Windows 10 Pro x64 auf die Hexadezimalzahl 55). Erst später folgt die Anweisung syscall. Sie veranlasst den Prozessor, in den Kernelmodus zu wechseln und zum Systemdienst-Dispatcher zu springen, wo mithilfe von EAX der Exekutivdienst NtCreateFile ausgewählt wird. Am Offset 0x308 wird außerdem das Flag 1 in SharedUserData geprüft (mehr über diese Struktur erfahren Sie in Kapitel 4). Ist es gesetzt, so folgt die Ausführung einem anderen Pfad und nutzt die Anweisung int 2Eh. Wenn Sie das VBS-Sondermerkmal CredentialGuard aktiviert haben (siehe Kapitel 7), wird das Flag auf dem Computer gesetzt, da der Hypervisor effizienter auf die Anweisung int reagieren kann als auf die Anweisung syscall. Dieses Verhalten ist für CredentialGuard von Vorteil. Weitere Einzelheiten über diesen Mechanismus (und über das Verhalten von syscall und int) finden Sie in Kapitel 8 von Band 2. Zunächst einmal aber können Sie versuchen, weitere native Dienste zu finden, z. B. NtReadFile, NtWriteFile und NtClose. Im Abschnitt »Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick« haben Sie bereits gelernt, dass IUM-Anwendungen eine ähnliche Binärdatei wie Ntdll.dll nutzen können, nämlich IumDll.dll. Diese Bibliothek enthält ebenfalls Systemaufrufe, allerdings mit abweichenden Indizes. Wenn auf Ihrem System CredentialGuard aktiviert ist, können Sie das vorstehende Experiment wiederholen, wobei Sie dieses Mal aber im Menü File von WinDbg auf Open Crash Dump klicken und dann die Datei IumDll.dll auswählen. In der folgenden Ausgabe ist das hohe Bit des Systemaufrufindex gesetzt. Die Überprüfung SharedUserData erfolgt jedoch nicht. Für diese Art von Systemaufrufen, die als sichere Systemaufrufe bezeichnet werden, wird stets die Anwendung syscall verwendet. 0:000> u iumdll!IumCrypto iumdll!IumCrypto: 00000001'80001130 4c8bd1 mov r10,rcx 00000001'80001133 b802000008 mov eax,8000002h 00000001'80001138 0f05 syscall 00000001'8000113a c3 ret Ntdll.dll enthält auch viele Unterstützungsfunktionen, z. B. den Abbildlader (Funktionen, deren Namen mit Ldr beginnen), den Heap-Manager und die Kommunikationsfunktionen des Windows-Teilsystemprozesses (Namen mit Csr). Des Weiteren gibt es in Ntdll.dll allgemeine Laufzeitbibliotheksroutinen (Rtl), Unterstützung für Debugging im Benutzermodus (DbgUi), 80 Kapitel 2 Systemarchitektur
Ereignisverfolgung für Windows (Etw) sowie den Dispatcher für asynchrone Prozeduraufrufe (APC) im Benutzermodus und den Ausnahme-Dispatcher. (APCs und Ausnahmen werden kurz in Kapitel 6 und ausführlicher in Kapitel 8 von Band 2 erklärt.) Auch einen kleinen Teil der C-Laufzeitroutinen (CRT) finden Sie in Ntdll.dll. Die Auswahl ist allerdings auf die Routinen beschränkt, die zur String- oder zur Standardbibliothek gehören (wie memcpy, strcpy, sprintf usw.). Diese Routinen sind für die im Folgenden beschriebenen nativen Anwendungen nützlich. Native Abbilder Einige Abbilder (ausführbare Dateien) gehören nicht zu irgendeinem Teilsystem. Sie werden also nicht mit einem Satz von Teilsystem-DLLs verlinkt (etwa Kernel32.dll für das Windows-Teilsystem), sondern nur mit Ntdll.dll. Dies ist der kleinste gemeinsame Nenner für alle Teilsysteme. Da die von Ntdll.dll bereitgestellte native API größtenteils nicht dokumentiert ist, werden Abbilder dieser Art gewöhnlich nur von Microsoft erstellt. Ein Beispiel dafür ist der Prozess des Sitzungs-Managers (Smss.exe; weiter hinten in diesem Kapitel ausführlicher beschrieben). Smss. exe ist der erste Benutzermodusprozess, der erstellt wird (direkt vom Kernel). Er kann daher nicht vom Windows-Teilsystem abhängen, da Csrss.exe (der Prozess des Windows-Teilsystems) noch gar nicht gestartet ist. Tatsächlich ist Smss.exe sogar dafür zuständig, Csrss.exe zu starten. Ein weiteres Beispiel ist das Dienstprogramm Autochk, das manchmal beim Systemstart ausgeführt wird, um Datenträger zu prüfen. Da es relativ im Startprozess läuft (gestartet von Smss. exe), kann es ebenfalls nicht von irgendeinem Teilsystem abhängen. Der folgende Screenshot aus Dependency Walker zeigt, dass Smss.exe nur von Ntdll.dll abhängig ist. Als Teilsystemtyp ist Native angegeben. Exekutive Die Windows-Exekutive bildet die obere Schicht von Ntoskrnl.exe. (Die untere Schicht ist der Kernel.) Sie enthält Funktionen der folgenden Art: QQ Funktionen, die exportiert werden und vom Benutzermodus aus aufgerufen werden können Diese Funktionen werden als Systemdienste bezeichnet und über Ntdll.dll exportiert (z. B. NtCreateFile aus dem vorherigen Experiment). Die meisten dieser Dienste sind über die Windows-API oder die APIs anderer Umgebungsteilsysteme zugänglich. Bei einigen wenigen jedoch ist kein Zugriff über irgendeine dokumentierte Teilsystemfunk­ tion möglich. (Dazu gehören beispielsweise ALPC und verschiedene Abfragefunktionen wie NtQueryInformationProcess, Sonderfunktionen wie NtCreatePagingFile usw.) Hauptsystemkomponenten Kapitel 2 81
QQ Gerätetreiberfunktionen, die über die Funktion DeviceIoControl aufgerufen werden können Dies ist eine allgemeine Schnittstelle vom Benutzer- zum Kernelmodus, um Funktionen in Gerätetreibern aufzurufen, die nichts mit Lesen oder Schreiben zu tun haben. Die Treiber für Process Explorer und Process Monitor von Sysinternals und der früher erwähnte Konsolentreiber (ConDrv.sys) sind gute Beispiele dafür. QQ Funktionen, die nur vom Kernelmodus aufgerufen werden können, exportiert werden und im WDK dokumentiert sind Dazu gehören mehrere Unterstützungsroutinen, die von Gerätetreiberentwicklern gebraucht werden, z. B. der E/A-Manager (Namen, die mit Io beginnen), allgemeine Exekutivfunktionen (Ex) usw. QQ Funktionen, die exportiert werden und vom Kernelmodus aufgerufen werden können, aber nicht im WDK dokumentiert sind Dazu gehören die Funktionen, die vom Bootvideotreiber aufgerufen werden und deren Namen mit Inbv beginnen. QQ Funktionen, die als globale Symbole definiert sind, aber nicht exportiert werden Dazu gehören interne Unterstützungsfunktionen, die innerhalb von Ntoskrnl.dll aufgerufen werden, z. B. diejenigen, deren Namen mit Iop (interne Unterstützungsfunktionen für den E/A-Manager) oder Mi beginnen (interne Unterstützungsfunktionen für die Speicherverwaltung). QQ Interne Funktionen eines Moduls, die nicht als globale Symbole definiert sind Diese Funktionen werden ausschließlich von der Exekutive und dem Kernel verwendet. Die Exekutive schließt die folgenden Hauptkomponenten ein, die alle in nachfolgenden Kapiteln dieses Buches noch ausführlich behandelt werden: QQ Konfigurations-Manager Der in Kapitel 9 von Band 2 beschriebene Konfigurations-Manager ist für die Implementierung und Verwaltung der Systemregistrierung zuständig. QQ Prozess-Manager Der in Kapitel 3 und 4 erklärte Prozess-Manager erstellt und beendet Prozesse und Threads. Die zugrunde liegende Unterstützung für Prozesse und Threads ist im Windows-Kernel implementiert. Die Exekutive ergänzt diese maschinennahen Objekte um zusätzliche Semantik und Funktionen. QQ Security Reference Monitor (SRM) Der in Kapitel 7 beschriebene SRM setzt Sicherheitsrichtlinien auf dem lokalen Computer durch. Er schützt Betriebssystemressourcen, führt einen Objektschutz zur Laufzeit und eine Überwachung durch. QQ E/A-Manager Der in Kapitel 6 beschriebene E/A-Manager implementiert die geräteunabhängige Ein-/Ausgabe und ist für die Zuteilung zu den passenden Gerätetreibern für die weitere Verarbeitung verantwortlich. QQ Plug-&-Play-Manager Der in Kapitel 6 beschriebene PnP-Manager entscheidet, welche Treiber für ein bestimmtes Gerät erforderlich sind, und lädt sie. Bei der Auflistung ruft er die Hardwareressourcenanforderungen für die einzelnen Geräte ab. Auf deren Grundlage weist er die entsprechenden Ressourcen wie E/A-Ports, IRQs, DMA-Kanäle und Speicherorte zu. Des Weiteren ist er dafür zuständig, ordnungsgemäße Ereignisbenachrichtigungen über Geräteänderungen (Hinzufügen oder Entfernen eines Geräts) im System zu senden. QQ Energie-Manager Der in Kapitel 6 erklärte Energie-Manager, die Prozessorenergieverwaltung (Power Process Management, PPM) und das Energieverwaltungsframework (Pow­ 82 Kapitel 2 Systemarchitektur
er Management Framework, PoFx) koordinieren Energieereignisse und erstellen Energieverwaltungsbenachrichtigungen für Gerätetreiber. Die PPM kann so eingerichtet werden, dass sie die CPU im Leerlauf in den Ruhezustand versetzt und damit die Leistungsaufnahme senkt. Änderungen im Stromverbrauch einzelner Geräte werden von den Gerätetreibern gehandhabt, aber vom Energie-Manager und dem PoFx koordiniert. Bei Geräten bestimmter Klassen verwaltet der Terminal Timeout Manager auch Zeitüberschreitungen der physischen Anzeige auf der Grundlage der Gerätenutzung und -nähe. QQ WDM-WMI-Routinen (Windows Driver Model / Windows Management Instrumentation) Diese in Kapitel 9 von Band 2 behandelten Routinen ermöglichen den Gerätetreibern, Leistungs- und Konfigurationsinformationen zu veröffentlichen und Befehle von WMI-Diensten im Benutzermodus zu empfangen. Verbraucher von WMI-Informationen können sich auf dem lokalen Computer, aber auch im Netzwerk befinden. QQ Speicher-Manager Der in Kapitel 5 beschriebene Speicher-Manager implementiert virtuellen Arbeitsspeicher. Dieses Speicherverwaltungsverfahren stellt für jeden Prozess einen umfangreichen privaten Adressraum zur Verfügung, der den verfügbaren physischen Arbeitsspeicher überschreiten kann. Des Weiteren bietet der Speicher-Manager auch Unterstützung für den Cache-Manager. Er wird vom Prefetcher und vom Store-Manager unterstützt, die beide ebenfalls in Kapitel 5 erläutert werden. QQ Cache-Manager Der in Kapitel 14 von Band 2 beschriebene Cache-Manager erhöht die Leistung der Datei-E/A, indem er dafür sorgt, dass erst vor Kurzem nachgeschlagene Daten von der Festplatte für den schnellen Zugriff im Hauptarbeitsspeicher abgelegt werden. Als weitere Maßnahme zur Leistungssteigerung verzögert er Schreibvorgänge auf der Festplatte, indem er Aktualisierungen für kurze Zeit im Arbeitsspeicher festhält und sie erst dann an die Festplatte sendet. Dazu nutzt er die Unterstützung des Speicher-Managers für zugeordnete Dateien. Daneben enthält die Exekutive Unterstützungsfunktionen, die von den zuvor genannten Komponenten genutzt werden. Ungefähr ein Drittel dieser Funktionen sind im WDK dokumentiert, da sie auch von den Gerätetreibern genutzt werden. Diese Unterstützungsfunktionen fallen in die folgenden vier Hauptkategorien: QQ Objekt-Manager Der Objekt-Manager erstellt, verwaltet und löscht Windows-Exekutivobjekte und abstrakte Datentypen, die zur Darstellung von Betriebssystemressourcen wie Prozessen, Threads und den verschiedenen Synchronisierungsobjekten dienen. Erklärt wird er in Kapitel 8 von Band 2. QQ ALPC-Einrichtung (Advanced Local Procedure Call) Die in Kapitel 8 von Band 2 er­ klärte ALPC-Einrichtung übergibt Nachrichten zwischen einem Client- und einem Serverprozess auf demselben Computer. Unter anderem dient sie als lokaler Transport für Remote­prozeduraufrufe (RPCs), die Windows-Implementierung einer standardmäßigen Kommunika­tionseinrichtung für Client- und Serverprozesse über ein Netzwerk. QQ Laufzeit-Bibliotheksfunktionen Dazu gehören Stringverarbeitung, arithmetische Operationen, Datentypumwandlung und Verarbeitung von Sicherheitsstrukturen. Hauptsystemkomponenten Kapitel 2 83
QQ Unterstützungsroutinen der Exekutive Dazu gehören die Systemspeicherzuweisung (ausgelagerter und nicht ausgelagerter Pool), gesperrter Speicherzugriff sowie besondere Arten von Synchronisierungsmechanismen wie Exekutivressourcen, schnelle Mutexe und Push-Sperren. Die Exekutive enthält auch eine Reihe weiterer Infrastrukturroutinen, von denen einige in diesem Buch nur kurz vorgestellt werden: QQ Kerneldebugger-Bibliothek Sie ermöglicht ein Debugging des Kernels mit einem Debugger, der KD unterstützt, ein portierbares Protokoll, das von verschiedenen Transporten wie USB, Ethernet und IEEE 1394 unterstützt und von WinDbg und Kd.exe implementiert wird. QQ Debugging-Framework für den Benutzermodus Dieses Framework sendet Ereignisse an die Debugging-API des Benutzermodus und ermöglicht Haltepunkte, ein schrittweises Durchlaufen des Codes und Kontextwechsel laufender Threads. QQ Hypervisor- und VBS-Bibliothek Diese Bibliotheken bieten Kernelunterstützung für die sichere VM-Umgebung und optimieren einzelne Teile des Codes, wenn das System weiß, dass es in einer Clientpartition (virtuellen Umgebung) läuft. QQ Errata-Manager Der Errata-Manager stellt Notlösungen für nicht standardmäßige und nicht konforme Hardwaregeräte zur Verfügung. QQ Treiberverifizierer Der Treiberverifizierer implementiert optionale Integritätsprüfungen von Treibern und Code im Kernelmodus (siehe Kapitel 6). QQ Ereignisverfolgung für Windows (ETW) ETW stellt Hilfsroutinen für die systemweite Ereignisverfolgung von Kernel- und Benutzermoduskomponenten bereit. QQ Windows Diagnostic Infrastructure (WDI) Die WDI ermöglicht eine intelligente Verfolgung von Systemaktivitäten auf der Grundlage von Diagnoseszenarien. QQ WHEA-Unterstützungsroutinen (Windows Hardware Error Architecture) Diese Routinen bilden ein allgemeines Framework für die Meldung von Hardwarefehlern. QQ File-System Runtime Library (FSRTL) tinen für Dateisystemtreiber. Die FSRTL bietet allgemeine Unterstützungsrou- QQ Kernel Shim Engine (KSE) Die KSE bietet Shims zur Treiberkompatibilität und zusätzliche Unterstützung für Gerätefehler. Sie nutzt die in Kapitel 8 von Band 2 beschriebene Shim-Infrastruktur und -Datenbank. Der Kernel Der Kernel besteht aus einem Satz Funktionen in Ntoskrnl.exe, die grundlegende Mechanismen bereitstellen. Dazu gehören die Threadplanungs- und Synchronisierungsdienste, die von den Exekutivkomponenten genutzt werden, sowie architekturabhängige, maschinennahe Unterstützung wie Interrupt- und Ausnahme-Dispatching, das auf jeder Prozessorarchitektur anders ausfällt. Der Kernelcode ist hauptsächlich in C geschrieben. Assemblercode ist für die Aufgaben reserviert, die Zugriff auf besondere, über C nicht zugängliche Prozessoranweisungen und Register benötigen. 84 Kapitel 2 Systemarchitektur
Wie bei den Unterstützungsfunktionen der Exekutive aus dem vorherigen Abschnitt sind auch eine Reihe der Funktionen im Kernel im WDK dokumentiert (und können durch eine Suche nach Funktionen gefunden werden, deren Namen mit Ke beginnen), da sie zur Implementierung von Gerätetreibern erforderlich sind. Kernelobjekte Der Kernel bietet eine maschinennahe Grundlage wohldefinierter, vorhersehbarer Betriebssystemprimitiva und -mechanismen, die es den Exekutivkomponenten höherer Ebene ermöglichen, ihre Aufgaben zu erfüllen. Der Kernel trennt sich selbst vom Rest der Exekutive, indem er Betriebssystemmechanismen implementiert und eine Aufstellung von Richtlinien vermeidet. Er überlässt nahezu alle Richtlinienentscheidungen der Exekutive. Die einzige Ausnahme bilden die Threadplanung und das Dispatching, die der Kernel implementiert. Außerhalb des Kernels stellt die Exekutive Threads und andere gemeinsam nutzbare Ressourcen als Objekte dar. Diese Objekte benötigen etwas Richtlinien-Overhead wie Objekthandles zu ihrer Handhabung, Sicherheitsprüfungen zu ihrem Schutz und Ressourcenkontingente, die bei ihrer Erstellung abgerufen werden müssen. Dieser Mehraufwand wird im Kernel vermieden, der einen Satz einfacherer Objekte implementiert, nämlich die Kernelobjekte. Sie helfen dem Kernel, die zentrale Verarbeitung zu steuern, und unterstützen die Erstellung von Exekutiv­objekten. Die meisten Exekutivobjekte kapseln ein oder mehrere Kernelobjekte und enthalten deren im Kernel definierte Attribute. Ein Satz von Kernelobjekten, die sogenannten Steuerobjekte, richten die Semantik für die Steuerung verschiedener Betriebssystemfunktionen ein. Dazu gehören das APC-Objekt (Asynchronous Procedure Call), das DPC-Objekt (Deferred Procedure Call) sowie mehrere Objekte, die der E/A-Manager verwendet, z. B. interrupt. Einen weiteren Satz von Kernelobjekten bilden die Dispatcherobjekte. Sie enthalten Synchronisierungsfähigkeiten, die die Threadplanung ändern oder beeinflussen. Zu den Dispatcherobjekten gehören der Kernelthread, Mutexe (in der Kernelterminologie Mutanten genannt), Ereignisse, Kernelereignispaare, Semaphoren, Timer und Timer mit Wartemöglichkeiten. Die Exekutive verwendet Kernelfunktionen, um Instanzen von Kernelobjekten zu erstellen, sie zu bearbeiten und die komplexeren Objekte zu konstruieren, die sie im Benutzermodus bereitstellt. Objekte werden ausführlicher in Kapitel 8 von Band 2 erklärt, Prozesse in Kapitel 3 und Threads in Kapitel 4. Die Steuerregion und der Steuerblock des Kernelprozessors Der Kernel verwendet eine als Steuerregion (Kernel Processor Control Region, KPCR) bezeichnete Datenstruktur, um prozessorspezifische Daten zu speichern. Die KPCR enthält grundlegende Informationen wie die Interruptdispatchtabelle (IDT) des Prozessors, sein Taskstatussegment (TSS) und die globale Deskriptortabelle (GDT). Außerdem schließt sie den Interruptcontroller­ status ein, den sie mit anderen Modulen teilt, darunter dem ACPI-Treiber und der HAL. Um leichten Zugang zur KPCR zu gewähren, speichert der Kernel einen Zeiger darauf, und zwar auf 32-Bit-Windows im Register fs und auf 64-Bit-Windows in gs. Die KPCR umfasst auch eine eingebettete Datenstruktur, die als Steuerblock des Kernelprozessors bezeichnet wird (Kernel Processor Control Block, KPRCB). Im Gegensatz zur KPCR, die für Drittanbieter-Treiber und andere interne Komponenten des Windows-Kernels dokumentiert ist, Hauptsystemkomponenten Kapitel 2 85
handelt es sich beim KPRCB um eine private Struktur, die nur vom Kernelcode in Ntoskrnl.exe verwendet wird. Sie enthält Folgendes: QQ Planungsinformationen wie den aktuellen, den nächsten sowie Leerlaufthreads, die zur Ausführung auf dem Prozessor geplant sind QQ Die Dispatcherdatenbank für den Prozessor, die wiederum die Bereitschaftswarteschlangen für jede Prioritätsebene enthält QQ Die DPC-Warteschlange QQ Angaben zur CPU und zu ihrem Hersteller wie Modell, Stepping-Geschwindigkeit und Feature-Bits QQ CPU- und NUMA-Topologie wie Knoteninformationen, Kerne pro Gehäuse, logische Prozessoren pro Kern usw. QQ Cachegrößen QQ Zeitabstimmungsinformationen wie die DPC- und die Interruptzeit Der KPRCB enthält auch alle Prozessorstatistiken, darunter die folgenden: QQ E/A-Statistiken QQ Cache-Manager-Statistiken (siehe Kapitel 14 in Band 2) QQ DPC-Statistiken QQ Speicher-Manager-Statistiken (siehe Kapitel 5) Manchmal wird der KPRCB dazu verwendet, am Cache ausgerichtete, prozessorweise Strukturen zur Optimierung des Speicherzugriffs zu speichern, insbesondere auf NUMA-Systemen, beispielsweise die Look-Aside-Listen des Systems für den nicht ausgelagerten und den ausgelagerten Pool. Experiment: KPCR und KPRCB einsehen Mit den Kerneldebuggerbefehlen !pcr und !prcb können Sie sich den Inhalt der KPCR und des KPRCB ansehen. Wenn Sie bei !prcb keine Flags angeben, zeigt der Debugger standardmäßig Informationen für CPU 0 an. Sie können jedoch auch eine bestimmte CPU auswählen, indem Sie eine Zahl an den Befehl anhängen, z. B. !prcb 2. Dagegen zeigt !pcr immer Informationen über den aktuellen Prozessor an. In einer Remotedebuggingsitzung können Sie den Prozessor wechseln. Beim lokalen Debugging können Sie die Adresse der KPCR gewinnen, indem Sie die Erweiterung !pcr gefolgt von der CPU-Nummer verwenden, und dann @$pcr durch diese Adresse ersetzen. Verwenden Sie keine der anderen Ausgaben von !pcr. Diese Erweiterung ist veraltet und zeigt keine korrekten Daten an. Das folgende Beispiel zeigt die Ausgabe von dt nt!_KPCR @$pcr und !pcrb auf Windows 10 x64: lkd> dt nt!_KPCR @$pcr +0x000 NtTib : _NT_TIB +0x000 GdtBase : 0xfffff802'a5f4bfb0 _KGDTENTRY64 +0x008 TssBase : 0xfffff802'a5f4a000 _KTSS64 → 86 Kapitel 2 Systemarchitektur
+0x010 UserRsp : 0x0000009b'1a47b2b8 +0x018 Self : 0xfffff802'a280a000 _KPCR +0x020 CurrentPrcb : 0xfffff802'a280a180 _KPRCB +0x028 LockArray : 0xfffff802'a280a7f0 _KSPIN_LOCK_QUEUE +0x030 Used_Self : 0x0000009b'1a200000 Void +0x038 IdtBase : 0xfffff802'a5f49000 _KIDTENTRY64 +0x040 Unused : [2] 0 +0x050 Irql : 0 '' +0x051 SecondLevelCacheAssociativity : 0x10 '' +0x052 ObsoleteNumber : 0 '' +0x053 Fill0 : 0 '' +0x054 Unused0 : [3] 0 +0x060 MajorVersion : 1 +0x062 MinorVersion : 1 +0x064 StallScaleFactor : 0x8a0 +0x068 Unused1 : [3] (null) +0x080 KernelReserved : [15] 0 +0x0bc SecondLevelCacheSize : 0x400000 +0x0c0 HalReserved : [16] 0x839b6800 +0x100 Unused2 : 0 +0x108 KdVersionBlock : (null) +0x110 Unused3 : (null) +0x118 PcrAlign1 : [24] 0 +0x180 Prcb : _KPRCB lkd> !prcb PRCB for Processor 0 at fffff803c3b23180: Current IRQL -- 0 Threads-- Current ffffe0020535a800 Next 0000000000000000 Idle fffff803c3b99740 Processor Index 0 Number (0, 0) GroupSetMember 1 Interrupt Count -- 0010d637 Times -- Dpc 000000f4 Interrupt 00000119 Kernel 0000d952 User 0000425d Sie können den Befehl dt auch verwenden, um die _KPRCB-Datenstrukturen direkt abzurufen, da Ihnen der Debuggerbefehl die Adresse dieser Strukturen gibt (in der vorstehenden Ausgabe fett hervorgehoben). Wenn Sie beispielsweise die beim Hochfahren ermittelte Prozessorgeschwindigkeit herausfinden wollen, können Sie sich mit dem folgenden Befehl das Feld MHz ansehen: lkd> dt nt!_KPRCB fffff803c3b23180 MHz +0x5f4 MHz : 0x893 lkd> ? 0x893 Evaluate expression: 2195 = 00000000'00000893 Hier lief der Prozessor beim Hochfahren also mit etwa 2,2 GHz. Hauptsystemkomponenten Kapitel 2 87
Hardwareunterstützung Die andere Hauptaufgabe des Kernels besteht darin, die Exekutive und die Gerätetreiber gegen Unterschiede zwischen den Hardwarearchitekturen abzuschirmen. Das schließt es auch ein, solche Abweichungen in Funktionen zu handhaben, z. B. für den Umgang mit Interrupts, das Ausnahme-Dispatching und die Synchronisierung mehrerer Prozessoren. Selbst bei solchen Funktionen, die sich auf die Hardware beziehen, wurde bei der Gestaltung des Kernels versucht, die Menge des gemeinsamen Codes zu maximieren. Der Kernel unterstützt eine Reihe von Schnittstellen, die architekturübergreifend portierbar und semantisch identisch sind. Der Großteil des Codes, der diese portierbaren Schnittstellen implementiert, ist ebenfalls in den verschiedenen Architekturen identisch. Einige dieser Schnittstellen sind auf einigen Architekturen unterschiedlich implementiert und verwenden teilweise architekturspezifischen Code. Diese architektonisch unabhängigen Schnittstellen können auf jedem Computer aufgerufen werden und sind unabhängig von den Codeunterschieden auf den Architekturen semantisch identisch. Manche Kernelschnittstellen, z. B. die in Kapitel 8 von Band 2 beschriebenen Spinlock-Routinen, sind in der HAL implementiert (siehe den nächsten Abschnitt), da ihre Implementierung sich schon zwischen Systemen derselben Architekturfamilie unterscheiden kann. Der Kernel enthält auch etwas Code mit x86-spezifischen Schnittstellen zur Unterstützung alter 16-Bit-MS-DOS-Programme (auf 32-Bit-Systemen). Diese x86-Schnittstellen sind nicht portierbar, allerdings nicht in dem Sinne, dass sie auf einem Computer mit einer anderen Architektur nicht aufgerufen werden können; tatsächlich sind sie dort nicht einmal vorhanden. Dieser x86-spezifische Code unterstützt beispielsweise Aufrufe zur Verwendung des virtuellen 8086-Modus, der zur Emulation von manchem Realmoduscode auf älteren Videokarten erforderlich ist. Ein weiteres Beispiel für architekturspezifischen Code im Kernel bilden die Schnittstellen für die Unterstützung des Übersetzungspuffers und des CPU-Cache. Aufgrund der Art und Weise, wie die Caches realisiert werden, ist dafür auf den einzelnen Architekturen jeweils unterschiedlicher Code erforderlich. Noch ein Beispiel bilden Kontextwechsel. Für Threadauswahl und Kontextwechsel wird zwar unabhängig von der Architektur der gleiche allgemeine Algorithmus verwendet (der Kontext des vorherigen Threads wird gespeichert, der Kontext des neuen Threads wird geladen und der neue Thread wird gestartet), aber bei der Implementierung in den Prozessoren gibt es architektonische Unterschiede. Da der Kontext durch den Prozessorzustand beschrieben wird (Register usw.), hängt es von der Architektur ab, was gespeichert und geladen wird. Die Hardwareabstraktionsschicht Einer der entscheidenden Aspekte des Windows-Designs ist die Portierbarkeit für verschiedene Hardwareplattformen. Angesichts von OneCore und den unzähligen Gerätebauformen ist dies heute wichtiger als je zuvor. Die Hardwareabstraktionsschicht (Hardware Abstraction Layer, HAL) ist die entscheidende Komponente, die diese Portierbarkeit ermöglicht. Es handelt sich dabei um ein ladbares Kernelmodusmodul (Hal.dll), das eine maschinennahe Schnittstelle zu der Hardwareplattform bietet, auf der Windows läuft. Sie deckt hardwareabhängige Einzel- 88 Kapitel 2 Systemarchitektur
heiten wie E/A-Schnittstellen, Interrupt-Controller und Mechanismen zur Kommunikation zwischen Prozessoren zu, also alle architekturspezifischen und maschinenabhängigen Funktionen. Die internen Windows-Komponenten und die Gerätetreiber bewahren diese Portierbarkeit, indem sie nicht direkt auf die Hardware zugreifen, sondern HAL-Routinen aufrufen, wenn sie plattformabhängige Informationen benötigen. Daher sind viele HAL-Routinen im WDK dokumentiert. Um mehr über die HAL und ihre Verwendung durch Gerätetreiber zu erfahren, schlagen Sie im WDK nach. In der Standard-Desktop-Installation von Windows sind zwei x86-HALs enthalten (siehe Tabelle 2–4). Zum Startzeitpunkt kann Windows erkennen, welche es verwenden soll. Dadurch wird das Problem gelöst, das bei früheren Versionen von Windows auftrat, wenn Sie versuchten, eine Windows-Installation auf einer anderen Art von System zu starten. HAL-Datei Unterstützte Systeme Halacpi.dll ACPI-PCs (Advanced Configuration and Power Interface). Damit sind Einprozessorcomputer ohne APIC-Unterstützung gemeint. (Sind mehrere Prozessoren oder ein APIC vorhanden, verwendet das System stattdessen die zweite HAL.) Halmacpi.dll APIC-PCs (Advanced Programmable Interrupt Controller) mit ACPI. Das Vorhandensein eines APIC bedeutet SMP-Unterstützung. Tabelle 2–4 x86-HALs Auf x64- und ARM-Computern gibt es nur ein HAL-Abbild, nämlich Hal.dll. Alle x64-Rechner benötigen sowohl ACPI- als auch APIC-Unterstützung und verfügen daher über dieselbe Motherboardkonfiguration, weshalb es nicht nötig ist, Computer ohne APIC oder mit einem Standard-PIC zu unterstützen. Auch alle ARM-Systeme verfügen über eine ACPI und verwenden Interruptcontroller, die einem Standard-APIC ähneln, weshalb eine einzige HAL für sie ausreicht. Allerdings sind diese Interruptcontroller einem Standard-APIC nur ähnlich, aber nicht identisch damit. Außerdem können sich die Timer und Speicher-/DMA-Controller auf einigen ARM-Systemen von denen auf anderen unterscheiden. Bei IoT-Geräten können sogar gängige PC-Hardwarekomponenten wie der Intel-DMA-Controller fehlen, weshalb sie selbst auf PC-gestützten Systemen Unterstützung für einen anderen Controller benötigen. Bei älteren Windows-Versionen wurden die Hersteller gezwungen, eine eigene HAL für jede mögliche Plattformkombination mitzuliefern. Das führt jedoch zu einer Unmenge von doppeltem Code und ist heute nicht mehr machbar. Stattdessen unterstützt Windows jetzt HAL-Erweiterungen. Dabei handelt es sich um zusätzliche DLLs auf der Festplatte, die der Bootloader laden kann, wenn sie von der Hardware benötigt werden. (Dies erfolgt gewöhnlich durch die ACPI und die Konfiguration in der Registrierung.) Ein Desktop-System mit Windows 10 verfügt gewöhnlich über die Erweiterungen HalExtPL080.dll und HalExtIntcLpioDMA.dll, wobei Letztere auf bestimmten Intel-Plattformen mit geringer Leistungsaufnahme verwendet wird. Die Entwicklung von HAL-Erweiterungen ist nur in Zusammenarbeit mit Microsoft möglich. Die Dateien müssen mit einem besonderen Zertifikat für HAL-Erweiterungen signiert werden, das es nur für Hardwarehersteller gibt. Außerdem ist die Auswahl der APIs, die sie nutzen können, stark eingeschränkt. Auch die Interaktion mit diesen APIs erfolgt durch einen eingeschränkten Import/Export-Tabellenmechanismus, der nicht den herkömmlichen PE-­ Hauptsystemkomponenten Kapitel 2 89
Abbildmechanismus verwendet. Wenn Sie das folgende Experiment mit einer HAL-Erweiterung ausführen, werden daher keine Funktionen angezeigt. Experiment: Abhängigkeiten von Ntoskrnl.exe und der HAL-Abbilder einsehen Sie können sich die Beziehung zwischen dem Kernel und den HAL-Abbildern mit Dependency Walker (Depends.exe) ansehen und ihre Export- und Importtabellen untersuchen. Öffnen Sie dazu in Dependency Walker das Menü File, klicken Sie auf Open und wählen Sie die gewünschte Abbilddatei aus. Die folgende Beispielausgabe zeigt die Abhängigen von Ntoskrnl.exe. (Ignorieren Sie zunächst die Fehlermeldungen, die dadurch zustande kommen, dass Dependency Walker nicht in der Lage ist, API-Sets zu analysieren.) Wie Sie sehen, ist Ntoskrnl.exe mit der HAL verknüpft und diese wiederum mit Ntoskrnl.exe. (Beide nutzen Funktionen der jeweils anderen Datei.) Außerdem ist Notskrnl.exe mit den folgenden Binärdateien verknüpft: QQ Pshed.dll Der PSHED (Platform-Specific Hardware Error Driver) bietet eine Abstraktion der Einrichtungen, die auf der zugrunde liegenden Plattform für die Meldung von Hardwarefehlern vorhanden sind. Dazu verbirgt er die Einzelheiten der Fehlerbehandlungsmechanismen der Plattform vor dem Betriebssystem und stellt diesem eine einheitliche Schnittstelle bereit. QQ Bootvid.dll Der Bootvideotreiber auf x86-Systemen (Bootvid) bietet Unterstützung für die VGA-Befehle, die erforderlich sind, um beim Hochfahren den Starttext und das Startlogo anzuzeigen. QQ Kdcom.dll Dies ist die Kommunikationsbibliothek des Kerneldebuggerprotokolls (KD). QQ Ci.dll Dies ist die Integritätsbibliothek (siehe Kapitel 8 in Band 2). → 90 Kapitel 2 Systemarchitektur
QQ Msrpc.sys Der Microsoft-RPC-Clienttreiber für den Kernelmodus ermöglicht dem Kernel (und anderen Treibern) die Kommunikation mit Benutzermodusdiensten über Remoteprozeduraufrufe (Remote Procedure Calls, RPCs) und das Marshalling von MES-codierten Daten. Beispielsweise nutzt der Kernel diesen Treiber für das Marshalling von Daten zum und vom Plug-&-Play-Dienst des Benutzermodus. Eine ausführliche Beschreibung der Informationen, die Dependency Walker ausgibt, finden Sie in der Hilfedatei dieses Tools (Depends.hlp). Wir haben Sie gebeten, die Fehlermeldungen zu ignorieren, die zustande kommen, da Dependency Walker keine API-Sets analysieren kann (die im Abschnitt über den Abbildlader in Kapitel 3 beschrieben werden). Das liegt daran, dass der Autor das Programm noch nicht aktualisiert hat, damit es auch diesen Mechanismus korrekt handhaben kann. Allerdings können Sie Dependency Walker immer noch nutzen, um sich anzusehen, welche Abhängigkeiten der Kernel je nach SKU noch haben kann, da die API-Sets auf echte Module verweisen. API-Sets sind keine DLLs oder Bibliotheken, sondern Kontrakte. Es kann durchaus sein, dass einige dieser Kontrakte (oder sogar alle) auf Ihrem Computer nicht vorhanden sind. Ihre Gegenwart hängt von SKU, Plattform und Hersteller ab. QQ Werkernel-Kontrakt Bietet Unterstützung für Windows Error Reporting (WER) im Kernel, z. B. beim Erstellen eines Live-Kernelauszugs. QQ Tm-Kontrakt Dies ist der Kernel-Transaktions-Manager (KTM), der in Kapitel 8 von Band 2 beschrieben wird. QQ Kcminitcfg-Kontrakt Verantwortlich für benutzerdefinierte Erstkonfiguration der Registrierung, die auf bestimmten Plattformen erforderlich sein kann. QQ Ksr-Kontrakt Kümmert sich um Kernel Soft Reboot (KSR) und die erforderliche Persistenz einzelner Speicherbereiche zu dessen Unterstützung, insbesondere auf bestimmten Mobil- und IoT-Plattformen. QQ Ksecurity-Kontrakt Enthält zusätzliche Richtlinien für AppContainer-Prozesse (also Windows-Apps) auf bestimmen Geräten und SKUs, die im Benutzermodus laufen. QQ Ksigningpolicy-Kontrakt Enthält zusätzliche Richtlinien für die Integrität von Benutzermoduscode (User-Mode Code Integrity, UMCI) zur Unterstützung von Nicht-AppContainer-Prozessen auf bestimmten SKUs oder zur ausführlicheren Konfiguration von DeviceGuard- und AppLocker-Sicherheitsmerkmalen auf bestimmten Plattformen und SKUs. QQ Ucode-Kontrakt Dies ist die Mikrocode-Aktualisierungsbibliothek für Plattformen, auf denen solche Aktualisierungen möglich sind, z. B. Intel und AMD. QQ Clfs-Kontrakt Dies ist der CLFS-Treiber (Common Log File System), der unter anderem vom Transaktionsregister (TxR) verwendet wird (siehe Kapitel 8 in Band 2). QQ Ium-Kontrakt Dies sind zusätzliche Richtlinien für IUM-Trustlets, die auf dem System laufen und von bestimmten SKUs gebraucht werden können, z. B. für abgeschirmte VMs auf einem Datacenter-Server. Trustlets werden ausführlicher in Kapitel 3 beschrieben. Hauptsystemkomponenten Kapitel 2 91
Gerätetreiber Gerätetreiber werden noch ausführlich in Kapitel 6 beschrieben. Dieses Kapitel gibt einen kurzen Überblick über die möglichen Arten von Treibern und zeigt, wie Sie die auf Ihrem System installierten und geladenen Treiber auflisten lassen können. Windows unterstützt sowohl Kernel- als auch Benutzermodustreiber, doch in diesem Abschnitt beschäftigen wir uns nur mit den Kerneltreibern. Der Begriff Gerätetreiber scheint auf Hardwaregeräte hinzudeuten, doch gibt es auch Gerätetreiber, die nicht direkt mit der Hardware zu tun haben (siehe die nachfolgende Liste). In diesem Abschnitt geht es hauptsächlich um Gerätetreiber zur Steuerung von Hardwaregeräten. Gerätetreiber sind ladbare Kernelmodusmodule (Dateien, die gewöhnlich die Endung .sys tragen), die als Schnittstelle zwischen dem E/A-Manager und der entsprechenden Hardware dienen. Sie werden im Kernelmodus in einem der drei folgenden Kontexte ausgeführt: QQ Im Kontext des Benutzerthreads, der eine E/A-Funktion (beispielsweise eine Leseoperation) ausgelöst hat QQ Im Kontext eines Kernelmodus-Systemthreads (z. B. einer Anforderung vom Plug-&-PlayManager) QQ Infolge eines Interrupts und daher nicht im Kontext eines bestimmten Threads, sondern des Threads, der beim Auftreten des Interrupts gerade ausgeführt wurde Wie bereits ausgeführt, handhaben Gerätetreiber in Windows die Hardware nicht direkt, sondern rufen Funktionen der HAL auf, um mit der Hardware zu arbeiten. Treiber sind gewöhnlich in C oder C++ geschrieben. Dank der Nutzung der HAL ist ihr Quellcode also über alle von Windows unterstützten CPU-Architekturen hinweg portierbar und ihre Binärdateien innerhalb der Architekturfamilie. Es gibt verschiedene Arten von Gerätetreibern: QQ Hardwaregerätetreiber Sie verwenden die HAL, um die Hardware zu beeinflussen und damit Ausgaben auf ein physisches Gerät oder das Netzwerk zu schreiben oder von dort abzurufen. Es gibt viele verschiedene Arten von Hardwaregerätetreibern, darunter Bustreiber, Benutzerschnittstellentreiber, Massenspeichertreiber usw. QQ Dateisystemtreiber Diese Windows-Treiber nehmen E/A-Anforderungen für Dateien entgegen und übersetzen sie in E/A-Anforderungen für ein bestimmtes Gerät. QQ Dateisystemfiltertreiber Dazu gehören die Treiber für Festplattenspiegelung und -verschlüsselung, die Untersuchung auf lokale Viren, das Abfangen von E/A-Anforderungen und eine Verarbeitung von E/A vor der Übergabe an die nächste Schicht (bzw. in manchen Fällen vor der Ablehnung einer solchen Operation). QQ Netzwerkredirectors und -server Dies sind Dateisystemtreiber, die Dateisystem-­E/AAnforderungen zu einem anderen Computer im Netzwerk übertragen bzw. solche Anforderungen von dort empfangen. QQ Protokolltreiber Sie implementieren Netzwerkprotokolle wie TCP/IP, NetBEUI oder IPX/SPX. QQ Kernelstreaming-Filtertreiber Diese Treiber sind verkettet, um eine Signalverarbeitung an Datenströme durchzuführen, z. B. Audio- und Videoaufnahme und -wiedergabe. 92 Kapitel 2 Systemarchitektur
QQ Softwaretreiber Diese Kernelmodule führen im Namen von Benutzermodusprozessen Operationen durch, die nur im Kernelmodus erledigt werden können. Viele Tools von Sys­ internals wie Process Explorer und Process Monitor verwenden Treiber, um Informationen abzurufen oder Operationen auszuführen, die mit Benutzermodus-APIs nicht zugänglich bzw. nicht möglich sind. Das Treibermodell von Windows Das ursprüngliche Treibermodell wurde in der ersten NT-Version (3.1) erstellt und bot noch keine Unterstützung für Plug & Play, da es dieses Funktionsprinzip damals noch gar nicht gab. Das blieb so bis Windows 2000 (bzw. Windows 95/98 auf der Seite der Windows-Versionen für Endverbraucher). In Windows 2000 kamen Unterstützung für Plug & Play, Energieoptionen und eine Erweiterung des Treibermodells von Windows NT hinzu, die als Windows Driver Model (WDM) bezeichnet wurde. Windows 2000 und höher kann ältere Windows NT 4-Treiber ausführen, aber da diese Treiber keine Unterstützung für Plug & Play und Energieoptionen bieten, sind die Möglichkeiten von Systemen, die diese Treiber nutzen, auf diesen beiden Gebieten eingeschränkt. Ursprünglich bot WDM ein gemeinsames Treibermodell, das zwischen Windows 2000/XP und Windows 98/ME (fast) quellcodekompatibel war. Dadurch wurde es einfacher, Treiber für Hardwaregeräte zu schreiben, da nur ein einziger Codestamm benötigt wurde und nicht zwei. Auf Windows 98/ME wurde WDM simuliert. Als diese Betriebssysteme außer Gebrauch kamen, blieb WDM das Grundmodell zum Schreiben von Hardwaregerätetreibern für Windows 2000 und höhere Versionen. WDM unterscheidet zwischen den drei folgenden Arten von Treibern: QQ Bustreiber Bustreiber sind für Buscontroller, Adapter, Brücken und jegliche anderen Geräte da, die über Kindgeräte verfügen. Diese Treiber sind erforderlich und werden im Allgemeinen von Microsoft bereitgestellt. Für jede Art von Bus auf einem System (wie PCI, PCMCIA und USB) gibt es einen eigenen Bustreiber. Drittanbieter können Bustreiber zur Unterstützung neuer Busse schreiben, z. B. für VMEbus, Multibus und Futurebus. QQ Funktionstreiber Ein Funktionstreiber ist der Hauptgerätetreiber, der die Betriebsschnittstelle für das zugehörige Gerät bereitstellt. Dieser Treiber ist erforderlich, sofern das Gerät nicht »roh« verwendet wird, wobei die E/A über den Bustreiber und Busfiltertreiber verfügt, z. B. einen SCSI-Pass-Thru-Treiber. Der Funktionstreiber ist per definitionem der Treiber, der am meisten über das zugehörige Gerät weiß, und gewöhnlich der einzige Treiber, der auf gerätespezifische Register zugreift. QQ Filtertreiber Filtertreiber dienen dazu, den Funktionsumfang eines Geräts oder eines vorhandenen Treibers zu erweitern oder um E/A-Anforderungen oder -Antworten von anderen Treibern zu ändern. Sie werden gewöhnlich als Korrekturmaßnahme verwendet, wenn Hardware fehlerhafte Informationen über ihre Ressourcenanforderungen bereitstellt. Filtertreiber sind optional. Sie können in beliebiger Anzahl auftreten, oberhalb oder unterhalb eines Funktionstreibers und oberhalb eines Bustreibers platziert werden. Gewöhnlich werden sie von den System-OEMs oder von unabhängigen Hardwareherstellern bereitgestellt. Hauptsystemkomponenten Kapitel 2 93
In der WDM-Treiberumgebung steuert kein einziger Treiber sämtliche Aspekte eines Geräts. Der Bustreiber meldet die Geräte, die sich an seinem Bus befinden, an den PnP-Manager, während der Funktionstreiber das Gerät handhabt. In den meisten Fällen ändern die Filtertreiber niedrigerer Ebene das Verhalten der Gerätehardware. Wenn beispielsweise ein Gerät an seinen Bustreiber meldet, dass es vier E/A-Ports benötigt, obwohl es in Wirklichkeit 16 sein müssten, kann ein gerätespezifischer Filtertreiber auf niedrigerer Ebene die vom Bustreiber an den PnP-Manager gemeldete Liste der Hardwareressourcen abfangen und die Anzahl der E/A-Ports korrigieren. Filtertreiber höherer Ebene dagegen dienen gewöhnlich dazu, den Funktionsumfang eines Geräts zu erweitern. Beispielsweise kann ein solcher Treiber für eine Festplatte zusätzliche Sicherheitsprüfungen erzwingen. Die Interruptverarbeitung wird allgemein in Kapitel 8 von Band 2 und im Zusammenhang mit Gerätetreibern in Kapitel 6 beschrieben. Auch der E/A-Manager, WDM, Plug & Play und die Energieverwaltung sind Thema von Kapitel 6. Windows Driver Foundation Die Windows Driver Foundation (WDF) vereinfacht die Entwicklung von Windows-Treibern, indem sie zwei Frameworks bereitstellt: das Kernel-Mode Driver Framework (KMDF) und das User-Mode Driver Framework (UMDF). Mit dem KMDF können Entwickler Treiber für Windows 2000 SP4 und höher schreiben, während UMDF Windows XP und höher unterstützt. KMDF stellt eine einfache Schnittstelle zum WDM bereit und verbirgt die Komplexität dieses Modells vor den Treiberautoren, ohne das zugrunde liegende Modell der Bus-, Funktionsund Filtertreiber zu ändern. KMDF-Treiber reagieren auf Ereignisse, die sie registrieren, und rufen Funktionen der KMDF-Bibliothek auf, um Arbeiten auszuführen, die nicht auf die von ihnen verwendete verwaltete Hardware zugeschnitten sind, z. B. allgemeine Energieverwaltung oder Synchronisierung. (Früher musste jeder Treiber all dies selbst implementieren.) Dabei kann ein einziger KDMF-Funktionsaufruf über 200 Zeilen WDM-Code einsparen. Mit UMDF ist es möglich, Treiber bestimmter Klassen – vor allem Treiber für Busse auf der Grundlage von USB oder anderen Protokollen mit hoher Latenz, z. B. für Videokameras, MP3-Player, Handys und Drucker – als Benutzermodustreiber zu implementieren. UMDF führt jeden Benutzermodustreiber letzten Endes in einem Benutzermodusdienst aus und verwendet ALPC zur Kommunikation mit einem Kernelmodus-Wrappertreiber, der den eigentlichen Zugang zur Hardware gewährt. Wenn ein UMDF-Treiber abstürzt, wird der Prozess beendet und gewöhnlich neu gestartet. Das System bleibt dabei stabil. Lediglich das Gerät ist vorübergehend nicht erreichbar, während der Dienst für den Treiber neu gestartet wird. Es gibt zwei Hauptversionen von UMDF: Version 1.x ist für alle Betriebssystemversionen verfügbar, die UMDF unterstützen. Die neueste Version ist 1.11 und steht in Windows 10 zur Verfügung. Für Version 1.x wurden C++ und COM zum Schreiben der Treiber verwendet, was für Benutzermodusprogrammierer sehr bequem ist, allerdings dazu führt, dass sich das UMDF-Modell von KMDF unterscheidet. Version 2.0 von UMDF wurde in Windows 8.1 eingeführt und basiert auf demselben Objektmodell wie KMDF, was die Programmiermodelle der beiden Frameworks sehr ähnlich macht. Der Quellcode des WDF wurde von Microsoft schließ- 94 Kapitel 2 Systemarchitektur
lich offengelegt und steht zurzeit auf GitHub unter https://github.com/Microsoft/Windows-­ Driver-Frameworks zur Verfügung. Universelle Windows-Treiber Mit dem in Windows 10 eingeführten Prinzip der universellen Windows-Treiber ist es möglich, Gerätetreiber zu schreiben, die die vom gemeinsamen Kern von Windows 10 bereitgestellten APIs und Gerätetreiberschnittstellen (Device Driver Interface, DDI) gemeinsam nutzen. Solche Treiber sind innerhalb einer CPU-Architektur (x86, x64 oder ARM) binärkompatibel und können auf Geräten mit unterschiedlicher Bauform verwendet werden, von IoT-Geräten über Smartphones, die HoloLens und die Xbox One bis zu Laptop- und Desktopcomputern. Als Treibermodell können universelle Treiber KMDF, UMDF 2.x und WDM nutzen. Experiment: Die installierten Gerätetreiber einsehen Um die installierten Gerätetreiber aufzulisten, führen Sie das Tool Systeminformationen (Msinfo32.exe) aus. Um es zu starten, klicken Sie auf Start und geben Msinfo32 ein. Erweitern Sie unter Systemübersicht den Knoten Softwareumgebung und darunter Systemtreiber. Die folgende Abbildung zeigt eine Beispielausgabe mit einer Liste der installierten Treiber. Dieses Fenster zeigt eine Liste der in der Registrierung definierten Gerätetreiber, ihres Typs und ihres Zustands (Wird ausgeführt oder Beendet) an. Gerätetreiber und Windows-Dienstprozesse werden beide in HKLM\SYSTEM\CurrentControlSet\Services definiert, aber durch den Typcode unterschieden. Beispielsweise handelt es sich bei Typ 1 um Kernelmodus-Gerätetreiber. Eine vollständige Liste der Informationen, die in der Registrierung über Gerätetreiber gespeichert sind, erhalten Sie in Kapitel 9 von Band 2. → Hauptsystemkomponenten Kapitel 2 95
Alternativ können Sie die zurzeit geladenen Gerätetreiber auch anzeigen lassen, indem Sie in Process Explorer den Prozess System auswählen und die DLL-Ansicht öffnen. Die folgende Abbildung zeigt eine Beispielausgabe. (Um zusätzliche Spalten anzuzeigen, rechtsklicken Sie auf einen Spaltenkopf und klicken auf Select Columns. Dadurch werden alle verfügbaren Spalten für die Module auf der DLL-Registerkarte angezeigt.) Ein Blick in nicht dokumentierte Schnittstellen Es kann sehr erhellend sein, einen genaueren Blick auf die Namen der exportierten oder globalen Symbole in den wichtigsten Systemabbildern (wie Ntoskrnl.exe, Hal.dll oder Ntdll. dll) zu werfen, denn dadurch erhalten Sie eine Vorstellung der Dinge, die Windows über das, was dokumentiert ist und zurzeit unterstützt wird, hinaus tun kann. Natürlich heißt das nicht, dass Sie diese Funktionen aufrufen können oder sollten, nur weil Sie ihre Namen kennen. Da die Schnittstellen nicht dokumentiert sind, können sie Änderungen unterworfen sein. Sehen Sie sich diese Funktionen nur an, um zu erkennen, welche Arten von internen Funktionen Windows ausführt, nicht um die unterstützten Schnittstellen zu umgehen. Wenn Sie sich beispielsweise die Funktionen in Ntdll.dll ansehen, finden Sie alle Systemdienste, die Windows für Teilsystem-DLLs im Benutzermodus bereitstellt – also nicht nur die Teilmenge, die das Teilsystem verfügbar macht. Viele dieser Funktionen sind zwar eindeutig dokumentierten und unterstützten Windows-Funktionen zugeordnet, aber einige von ihnen stehen nicht über die Windows-API zur Verfügung. Es ist allerdings auch interessant, sich die Importe der Windows-Teilsystem-DLLs (wie Kernel32.dll oder Advapi32.dll) und die Funktionen anzusehen, die sie in Ntdll.dll aufrufen. Auch eine Untersuchung von Ntoskrnl.exe ist interessant: Viele der exportierten Routinen, die von Kernelmodus-Gerätetreibern genutzt werden, sind zwar im WDK dokumentiert, doch eine ganze Menge von ihnen nicht. Schauen Sie sich auch die Importtabelle für Ntoskrnl.exe und die HAL an. Sie zeigt die Funktionen in der HAL, die Ntoskrnl.exe nutzt, und umgekehrt. → 96 Kapitel 2 Systemarchitektur
Tabelle 2–5 führt die meisten gängigen Funktionsnamenpräfixe für Komponenten der Exekutive auf. Jede dieser Komponenten verwendet auch eine Variante des Präfixes, um interne Funktionen zu kennzeichnen. Dabei wird an den ersten Buchstaben des Präfix­ es entweder ein i (für intern) oder an das komplette Präfix ein p (für privat) angehängt. Beispielsweise steht Ki für interne Kernelfunktionen und Psp für interne Prozessunterstützungsfunktionen. Präfix Komponente Alpc Advanced Local Procedure Calls Cc Gemeinsamer Cache (Common Cache) Cm Konfigurations-Manager Dbg Kernel-Debugunterstützung Dbgk Debugging-Framework für den Benutzermodus Em Errata-Manager Etw Ereignisverfolgung für Windows Ex Unterstützungsroutinen der Exekutive FsRtl Dateisystem-Laufzeitbibliothek Hv Hive-Bibliothek Hvl Hypervisor-Bibliothek Io E/A-Manager Kd Kerneldebugger Ke Kernel Kse Kernel Shim Engine Lsa Lokale Sicherheitsautorität Mm Speicher-Manager Nt NT-Systemdienste (vom Benutzermodus aus über Systemaufrufe zugänglich) Ob Objekt-Manager Pf Prefetcher Po Energie-Manager PoFx Energie-Framework Pp PnP-Manager Ppm Prozessor-Energie-Manager Ps Prozessunterstützung Rtl Laufzeitbibliothek Se Security Reference Monitor Sm Store-Manager Tm Transaktions-Manager Ttm Terminal Timeout Manager → Hauptsystemkomponenten Kapitel 2 97
Präfix Komponente Vf Treiberverifizierer Vsl Bibliothek für Virtual Secure Mode Wdi Windows Diagnostic Infrastructure Wfp Windows FingerPrint Whea Windows Hardware Error Architecture Wmi Windows Management Instrumentation Zw Spiegeleintrittspunkt für Systemdienste (die mit Nt beginnen). Er setzt den vorherigen Zugriffsmodus auf den Kernelmodus. Dadurch fällt die Parametervalidierung weg, da Nt-Systemdienste Parameter nur dann überprüfen, wenn der vorherige Zugriffsmodus der Benutzermodus war. Tabelle 2–5 Gängige Präfixe Die Namen der exportierten Funktionen können Sie einfacher deuten, wenn Sie die Namenskonvention für Windows-Systemroutinen kennen. Das allgemeine Format lautet: <Präfix><Operation><Objekt> Präfix gibt die interne Komponente an, die die Routine exportiert, Operation sagt, was mit dem Objekt oder der Ressource geschieht, und Objekt nennt das Objekt, an dem die Operation ausgeführt wird. Beispielsweise ist ExAllocatePoolWithTag die Unterstützungsroutine der Exekutive für die Zuweisung vom ausgelagerten oder nicht ausgelagerten Pool und KeInitializeThread die Routine, die ein Kernelthreadobjekt zuweist und einrichtet. Systemprozesse Die folgenden Systemprozesse erscheinen auf jedem Windows 10-System. Einer davon (der Leerlaufprozess) ist in Wirklichkeit gar kein Prozess, und drei von ihnen – System, Secure System und Speicherkomprimierung – sind keine vollständigen Prozessen, da sie nicht in einer ausführbaren Benutzermodusdatei laufen. Prozesse dieser Art werden minimale Prozesse genannt und werden in Kapitel 3 erklärt. QQ Leerlaufprozess tigen. Enthält einen Thread pro CPU, um CPU-Leerlaufzeiten zu berücksich- QQ Systemprozess Enthält den Großteil der Kernelmodus-Systemthreads und -handles. QQ Secure-System-Prozess ser ausgeführt wird. Enthält den Adressraum des sicheren Kernels in VTL 1, falls die- QQ Speicherkomprimierungsprozess Enthält den komprimierten Arbeitsteil der Benutzermodusprozesse (siehe Kapitel 5). QQ Sitzungs-Manager Smss.exe QQ Windows-Teilsystem Csrss.exe 98 Kapitel 2 Systemarchitektur
QQ Sitzung-0-Initialisierung Wininit.exe QQ Anmeldeprozess Winlogon.exe QQ Dienststeuerungs-Manager Services.exe und die von ihm erzeugten Kindprozesse wie der vom System bereitgestellte, allgemeine Diensthostprozess (Svchhost.exe). QQ Lokaler Sicherheitsauthentifizierungsdienst Lsass.exe; falls CredentialGuard aktiviert ist, auch Lsaiso.exe (Isolated Local Security Authentication Server). In der Prozessstruktur können Sie die Beziehungen zwischen den Prozessen erkennen und erfahren, von welchen Prozessen die einzelnen Prozesse erstellt wurden. Abb. 2–6 zeigt die Prozessstruktur in einer Startablaufverfolgung von Process Monitor. Um eine solche Ablaufverfolgung durchzuführen, öffnen Sie in Process Monitor das Menü Options und wählen Enable Boot Logging. Starten Sie das System dann neu, öffnen Sie Process Monitor wieder und wählen Sie im Menü Tools den Punkt Process Tree oder drücken Sie (Strg) + (T). In Process Monitor können Sie auch die inzwischen beendeten Prozesse erkennen – sie werden durch ein verblasstes Symbol dargestellt. Abbildung 2–6 Die anfängliche Prozessstruktur In den nächsten Abschnitten werden die wichtigsten Systemprozesse erklärt, die Sie in Abb. 2–6 sehen. Dabei wird auch die Reihenfolge erläutert, in der die Prozesse gestartet werden. Eine ausführliche Beschreibung der Schritte, die beim Hochfahren von Windows erfolgen, finden Sie in Kapitel 11 von Band 2. Hauptsystemkomponenten Kapitel 2 99
Der Leerlaufprozess Der erste in Abb. 2–6 aufgeführte Prozess ist der Leerlaufprozess (Idle). Wie in Kapitel 3 erklärt, werden Prozesse anhand ihres Abbildnamens bezeichnet. Allerdings führt dieser Prozess – ebenso wie der Systemprozess, der Secure-System-Prozess und der Speicherkomprimierungsprozess – kein echtes Benutzermodusabbild aus, d. h., im Verzeichnis \Windows gibt es keine Datei namens System Idle Process.exe. Aufgrund der Art der Implementierung wird dieser Prozess (ID 0) auch je nach verwendetem Programm unter einem anderen Namen angezeigt (siehe Tabelle 2–6). Der Leerlaufprozess dient dazu, Leerlaufzeiten zu berücksichtigen. Daher ist die Anzahl der »Threads« in diesem »Prozess« die Anzahl der logischen Prozessoren des Systems. In Kapitel 3 finden Sie eine ausführlichere Erklärung dieses Prozesses. Programme Name für den Prozess mit der ID 0 Task-Manager Systemleerlaufprozess (System Idle process) Process Status (Pstat.exe) Idle process Process Explorer (Procexp.exe) System Idle process Task List (Tasklist.exe) System Idle process Tlist (Tlist.exe) System process Tabelle 2–6 Namen des Prozesses mit der ID 0 in verschiedenen Programmen Als Nächstes sehen wir uns die Systemthreads und den Zweck der einzelnen Systemprozesse an, die echte Abbilder ausführen. Der Systemprozess und die Systemthreads Der Systemprozess (Prozess-ID 4) ist das Zuhause einer besonderen Art von Threads, die nur im Kernelmodus laufen – der Kernelmodus-Systemthreads. Sie haben alle Attribute und Kontexte regulärer Benutzermodusthreads, z. B. Hardwarekontext, Priorität usw., laufen aber ausschließlich im Kernelausführungscode, der in den Systemraum geladen ist, unabhängig davon, ob dies in Ntoskrnl.ee oder einem anderen geladenen Gerätetreiber ist. Außerdem haben Systemthreads keinen Benutzerprozess-Adressraum und müssen zur Zuweisung von dynamischem Speicher daher auf Speicherheaps des Betriebssystems zurückgreifen, z. B. auf den ausgelagerten oder den nicht ausgelagerten Pool. Im Task-Manager von Windows 10 Version 1511 wird der Systemprozess als Systemspeicher und komprimierter Speicher bezeichnet. Der Grund dafür ist ein neues Merkmal von Windows 10, das den Arbeitsspeicher komprimiert, um dort mehr Prozessinformationen unterzubringen, anstatt sie auf die Festplatte auszulagern. Dieser Mechanismus wird ausführlicher in Kapitel 5 beschrieben. Merken Sie sich nur, dass sich der Begriff Systemprozess auf diesen Prozess bezieht, wie auch immer er in irgendeinem Programm angezeigt werden mag. In Windows 10 Version 1607 und Windows Server 2016 heißt der Prozess übrigens wieder einfach System, da für die Speicherkomprimierung hier der Prozess Speicherkomprimierung verwendet wird. Auch dieser Prozess wird in Kapitel 5 ausführlicher beschrieben. 100 Kapitel 2 Systemarchitektur
Systemthreads werden von der Funktion PsCreateSystemThread oder IoCreateSystemThreads erstellt, die beide im WDK dokumentiert sind. Diese Threads können nur vom Kernelmodus aus aufgerufen werden. Windows und verschiedene Gerätetreiber erstellen Systemthreads bei der Systeminitialisierung, um Operationen auszuführen, die einen Threadkontext benötigen, z. B. um Ein-/Ausgaben oder andere Objekte auszugeben und darauf zu warten oder Informationen über ein Gerät abzufragen. So nutzt beispielsweise der Speicher-Manager Systemthreads zur Implementierung von Funktionen, die modifizierte Seiten in die Auslagerungsdatei oder in zugeordnete Dateien schreiben, um Prozesse aus dem Arbeitsspeicher auszulagern und wieder dorthin zu übernehmen usw. Der Kernel erstellt den Systemthread Balance-Set-Manager, der einmal pro Sekunde erwacht, um ggf. verschiedene Planungs- und Speicherverwaltungsereignisse auszulösen. Auch der Cache-Manager verwendet Systemthreads, nämlich für vorausschauendes Lesen und verzögertes Schreiben. Der Dateiserver-Gerätetreiber (Srv2.sys) nutzt Systemthreads, um auf Netzwerk-E/A-Anforderungen für Dateidaten auf Netzwerkfreigaben zu reagieren. Selbst der Plattentreiber hat einen Systemthread, um das Laufwerk abzufragen. (Die Abfrage ist in diesem Fall effizienter, da ein interruptgesteuerter Plattentreiber große Mengen an Systemressourcen verbraucht.) Weitere Informationen über einzelne Systemthreads finden Sie jeweils in den Kapiteln, in denen die zugehörigen Komponenten beschrieben werden. Standardmäßig gehören Systemthreads dem Systemprozess, doch können Gerätetreiber Systemthreads auch in beliebigen Prozessen erstellen. Beispielsweise legt der Gerätetreiber für das Windows-Teilsystem (Win32k.sys) einen Systemthread im kanonischen Displaytreiber (Cdd. dll) des Windows-Teilsystemprozesses (Csrss.exe) an, sodass Daten im Benutzermodus-Adressraum leicht auf diesen Prozess zugreifen können. Bei einer Störungssuche oder bei einer Systemanalyse ist es praktisch, die einzelnen Systemthreads zu den Treibern oder sogar den Subroutinen zurückverfolgen zu können, die den Code enthalten. Beispielsweise verbraucht der Systemprozess auf einem schwer belasteten Dateiserver wahrscheinlich erheblich viel CPU-Zeit. Zu wissen, dass »irgendein Systemthread« läuft, wenn der Systemprozess ausgeführt wird, reicht nicht aus, um zu erkennen, welcher Gerätetreiber oder welche Betriebssystemkomponente läuft. Daher müssen Sie als Erstes herausfinden, welche Systemthreads ausgeführt werden (z. B. mit der Leistungsüberwachung oder mit Process Explorer). Sobald Sie sie gefunden haben, können Sie nachschauen, in welchem Treiber ihre Ausführung begonnen hat. Das verrät Ihnen zumindest, welcher Treiber den Thread wahrscheinlich erstellt hat. Beispielsweise können Sie in Process Explorer auf den Systemprozess rechtsklicken, Properties auswählen und dann auf der Registerkarte Threads auf den Spaltenkopf CPU klicken, damit der aktivste Thread ganz oben angezeigt wird. Markieren Sie diesen Thread und klicken Sie auf die Schaltfläche Module, um sich die Datei mit dem Code anzeigen zu lassen, der auf der Spitze des Stacks ausgeführt wird. Da der Systemprozess in den neuen Versionen von Windows geschützt ist, kann Process Explorer den Aufrufstack nicht anzeigen. Der Secure-System-Prozess Der Secure-System-Prozess (variable Prozess-ID) ist technisch gesehen das Zuhause des Adress­ raums, der Handles und Systemthreads des sicheren VLT-1-Kernels. Da Planung, Objekt- und Speicherverwaltung jedoch im Besitz des VTL-0-Kernels sind, werden solche Entitäten in Wirk- Hauptsystemkomponenten Kapitel 2 101
lichkeit gar nicht diesem Prozess zugeordnet. Sein einziger tatsächlicher Zweck besteht darin, in Werkzeugen wie dem Task-Manager oder Process Explorer einen sichtbaren Indikator dafür bereitzustellen, dass VBS zurzeit aktiv ist (und mindestens eines der Merkmale bereitstellt, die ihn nutzen). Der Speicherkomprimierungsprozess Der Speicherkomprimierungsprozess verwendet seinen Benutzermodus-Adressraum, um die komprimierten Speicherseiten zu speichern, die dem aus den Arbeitssätzen bestimmter Prozesse getilgtem Standby-Speicher entsprechen (siehe Kapitel 5). Anders als der Secure-System-Prozess beherbergt der Speicherkomprimierungsprozess tatsächlich eine Reihe von Systemthreads, die gewöhnlich als SmKmStoreHelperWorker und SmStReadThread angezeigt werden. Beide gehören zum Store-Manager, der sich um die Speicherkomprimierung kümmert. Im Gegensatz zu den anderen Systemprozessen in der Liste befindet sich der Arbeitsspeicher dieses Prozesses tatsächlich im Benutzermodus-Adressraum. Daher unterliegt er der Beschneidung des Arbeitssatzes und wird in Systemüberwachungswerkzeugen möglicherweise mit einer umfangreichen Speichernutzung angezeigt. Die Registerkarte Leistung des Task-Managers zeigt jetzt sowohl den verwendeten als auch den komprimierten Arbeitsspeicher. Dort können Sie nun erkennen, dass der Arbeitssatz des Speicherkomprimierungsprozesses genauso groß ist wie der komprimierte Arbeitsspeicher. Der Sitzungs-Manager Der Sitzungs-Manager (%SystemRoot%\System32\Smss.exe) ist der erste Benutzermodusprozess, der im System erstellt wird, und zwar von dem Kernelmodus-Systemthread, der die letzte Phase der Initialisierung der Exekutive und des Kernels durchführt. Wie in Kapitel 3 beschrieben, wird er als PPL-Prozess angelegt (Protected Process Light). Bei seinem Start prüft Smss.exe, ob er die erste Instanz (der Master) oder eine Instanz ist, die der Master erzeugt hat, um eine Sitzung zu erstellen. Sind Befehlszeilenargumente vorhanden, so ist Letzteres der Fall. Wenn Smss.exe beim Hochfahren oder beim Erstellen einer Terminaldienste-Sitzung mehrere Instanzen von sich selbst anlegt, kann er dadurch mehrere Sitzungen gleichzeitig erzeugen – bis zu vier gleichzeitige plus eine zusätzliche für jede CPU, die es über die erste hinaus noch gibt. Das verbessert die Anmeldeleistung auf Terminal-Servern, wenn mehrere Benutzer gleichzeitig Verbindung aufnehmen. Wenn eine Sitzung die Initialisierung abgeschlossen hat, wird die Kopie von Smss.exe beendet, sodass nur der ursprüngliche Smss.exe-Prozess aktiv bleibt. (Eine Beschreibung der Terminaldienste finden Sie im Abschnitt »Terminaldienste und mehrere Sitzungen« von Kapitel 1.) Der Smss.exe-Masterprozess führt folgende einmalige Initialisierungsschritte aus: 1. Er kennzeichnet den Prozess und den ursprünglichen Thread als kritisch. Wird ein solcher Prozess oder Thread aus irgendeinem Grunde beendet, stürzt Windows ab. Mehr darüber erfahren Sie in Kapitel 3. 2. Er sorgt dafür, dass der Prozess bestimmte Fehler als kritisch behandelt, z. B. die Verwendung ungültiger Handles oder eine Beschädigung des Heaps, und aktiviert den Prozess­ ausgleich Disable Dynamic Code Execution. 102 Kapitel 2 Systemarchitektur
3. Er erhöht die Basispriorität des Prozesses auf 11. 4. Wenn das System ein Hinzufügen von Prozessoren im laufenden Betrieb erlaubt, aktiviert er die automatische Aktualisierung der Prozessoraffinität. Wird dann ein neuer Prozessor hinzugefügt, kann er von neuen Sitzungen genutzt werden. Mehr über das dynamische Hinzufügen von Prozessoren erfahren Sie in Kapitel 4. 5. Er initialisiert einen Threadpool zur Handhabung von ALPC-Befehlen und anderen Arbeitselementen. 6. Er erstellt den ALPC-Port \SmApiPort, um Befehle zu empfangen. 7. Er initialisiert eine lokale Kopie der NUMA-Topologie des Systems. 8. Er erstellt den Mutex PendingRenameMutex, um Operationen zum Umbenennen von Dateien zu synchronisieren. 9. Er erstellt den ursprünglichen Prozessumgebungsblock und aktualisiert bei Bedarf die Variable Safe Mode. 10. Er erstellt Sicherheitsdeskriptoren für verschiedene Systemressourcen auf der Grundlage des Wertes von ProtectionMode in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager. 11. Er erstellt die beschriebenen Objekt-Manager-Verzeichnisse auf der Grundlage des Wertes von ObjectDirectories in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager, z. B. \RPC Control und \Windows. Des Weiteren speichert er die in den Werten BootExecute, BootExecuteNoPnpSync und SetupExecute aufgeführten Programme. 12. Er speichert den Programmpfad, der im Wert S0InitialCommand unter HKLM\SYSTEM\ CurrentControlSet\Control\Session Manager angegeben ist. 13. Er liest den Wert NumberOfInitialSessions von HKLM\SYSTEM\CurrentControlSet\Control\ Session Manager, ignoriert ihn aber, wenn sich das System im Fertigungsmodus befindet. 14. Er liest die Dateiumbenennungsoperationen, die in den Werten PendingFileRenameOperations und PendingFileRenameOperations2 von HKLM\SYSTEM\CurrentControlSet\Control\ Session Manager aufgeführt sind. 15. Er liest die Werte von AllowProtectedRenames, ClearTempFiles, TempFileDirectory und D­ isableWpbtExecution in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager. 16. Er liest die Liste der DLLs im Wert ExcludeFromKnownDllList von HKLM\SYSTEM\Current­ ControlSet\Control\Session Manager. 17. Er liest die Informationen über die Auslagerungsdatei, die in den Werten PagingFiles und ExistingPageFileslist und den Konfigurationswerten PagefileOnOsVolume und WaitFor­PagingFiles von HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management gespeichert sind. 18. Er liest und speichert die Werte unter HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\ DOS Devices. 19. Er liest und speichert die Liste aus dem Wert KnownDlls in HKLM\SYSTEM\CurrentControlSet\ Control\Session Manager. 20. Er erstellt systemweite Umgebungsvariablen nach der Definition in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment. Hauptsystemkomponenten Kapitel 2 103
21. Er erstellt das Verzeichnis \KnownDlls und auf 64-Bit-Systemen mit WoW64 auch \Known­​ Dlls32. 22. Er erstellt symbolische Links für die in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\DOS Devices definierten Geräte im Verzeichnis \Global?? des Objekt-Manager-­ Namensraums. 23. Er erstellt das Stammverzeichnis \Sessions im Namensraum des Objekt-Managers. 24. Er erstellt Präfixe für geschützte Mailslots und benannte Pipes, um Dienstanwendungen gegen Spoofing-Angriffe zu schützen, die vorkommen können, wenn schädliche Benutzermodusanwendungen vor einem Dienst ausgeführt werden. 25. Er führt die Programme aus, die auf den zuvor analysierten Listen BootExecute und BootExecuteNoPnpSync aufgeführt sind. (Standardmäßig wird Autochk.exe ausgeführt, das die Festplatte überprüft.) 26. Er initialisiert den Rest der Registrierung (die Hives Software, SAM und Security von HKLM). 27. Sofern sie nicht in der Registrierung deaktiviert ist, führt er die WPBT-Binärdatei (Windows Platform Binary Table) aus, die in der zugehörigen ACPI-Tabelle registriert ist. Dies wird oft von Diebstahlsschutzeinrichtungen genutzt, um sehr früh eine native Windows-Binärdatei auszuführen, die selbst auf frisch installierten Systemen nach Hause telefonieren oder andere Dienste zur Ausführung einrichten kann. Diese Prozesse dürfen nur mit Ntdll.dll verlinkt sein (also zum nativen Teilsystem gehören). 28. Er verarbeitet ausstehende Umbenennungen von Dateien, die in den zuvor untersuchten Registrierungsschlüsseln angegeben wurden, sofern es sich nicht um einen Start in die Windows-Wiederherstellungsumgebung handelt. 29. Er initialisiert Informationen über Auslagerungsdateien und dedizierte Dumpdateien auf der Grundlage von HKLM\System\CurrentControlSet\Control\Session Manager\Memory ­Management und HKLM\System\CurrentControlSet\Control\CrashControl. 30. Er prüft die Kompatibilität des Systems mit Speicherkühltechniken, wie sie auf NUMA-­ Systemen verwendet werden. 31. Er speichert die alte Auslagerungsdatei, erstellt die dedizierte Absturzabbilddatei sowie nach Bedarf neue Auslagerungsdateien auf der Grundlage der vorherigen Absturzinformationen. 32. Er erstellt zusätzliche dynamische Umgebungsvariablen wie PROCESSOR_ARCHITECTURE, P­ ROCESSOR_LEVEL, PROCESSOR_IDENTIFIER und PROCESSOR_REVISION auf der Grundlage von Registrierungsinformationen sowie aus dem Kernel abgerufener Systeminformationen. 33. Er führt die Programme in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\­ SetupExecute aus. Dabei gelten die gleichen Regeln wie für BootExecute in Schritt 11. 34. Er erstellt ein unbenanntes Abschnittsobjekt, das von den Kindprozessen (z. B. Csrss.exe) für die mit Smss.exe ausgetauschten Informationen gemeinsam genutzt wird. Das Handle für diesen Abschnitt wird durch Vererbung an die Kindprozesse übergeben. Mehr über die Vererbung von Handles erfahren Sie in Kapitel 8 von Band 2. 104 Kapitel 2 Systemarchitektur
35. Er öffnet bekannte DLLs und ordnet sie als permanente Abschnitte (zugeordnete Dateien) zu – bis auf diejenigen, die bei früheren Registrierungschecks als Ausnahmen aufgeführt wurden. (Standardmäßig sind keine Ausnahmen vorhanden.) 36. Er erstellt einen Thread, um auf Anforderungen zum Erstellen von Sitzungen zu reagieren. 37. Er erstellt eine Instanz von Smss.exe, um Sitzung 0 (die nicht interaktive Sitzung) zu initialisieren. 38. Er erstellt eine Instanz von Smss.exe, um Sitzung 1 (die interaktive Sitzung) zu initialisieren. Bei entsprechender Konfiguration in der Registrierung erstellt er weitere Smss.exe-Instanzen für zusätzliche interaktive Sitzungen, um sich auf künftige Benutzeranmeldungen vorzubereiten. Wenn Smss.exe diese Instanzen erstellt, fordert er jedes Mal mit dem Flag PROCESS_CREATE_NEW_SESSION in NtCreateUserProcess ausdrücklich eine neue Prozess-ID an. Dadurch wird die interne Speicher-Manager-Funktion MiSessionCreate aufgerufen. Sie erstellt die erforderlichen Kernelmodus-Sitzungsdatenstrukturen (wie das Sitzungsobjekt) und richtet den virtuellen Sitzungsadressraum ein, der vom Kernelmodusteil des Windows-Teilsystems (Win32k.sys) und anderen Sitzungsraum-Gerätetreibern genutzt wird (siehe Kapitel 5). Nach Abschluss dieser Schritte wartet Smss.exe unaufhörlich am Handle für die Instanz der Sitzung 0 auf Csrss.exe. Da Csrss.exe als kritischer Prozess gekennzeichnet ist (also als geschützter Prozess; siehe Kapitel 3), wird dieser Wartevorgang bei einer Beendigung von Csrss.exe niemals abgeschlossen, da das System dann abstürzt. Eine Instanz von Smss.exe für den Start einer Sitzung macht Folgendes: QQ Sie erstellt die Teilsystemprozesse für die Sitzung (standardmäßig den Windows-Teilsystemprozess Csrss.exe). QQ Sie erstellt eine Instanz von Winlogon (interaktive Sitzungen) oder des Anfangsbefehls für Sitzung 0. Bei Letzterem handelt es sich standardmäßig um Wininit, sofern in den zuvor untersuchten Registrierungswerten nichts anderes angegeben ist. Weitere Informationen über diese beiden Prozesse finden Sie in den folgenden Abschnitten. Schließlich wird dieser Smss.exe-Zwischenprozess beendet und lässt die Teilsystem-Prozesse und Winlogon oder Wininit ohne Elternprozess zurück. Der Windows-Initialisierungsprozess Der Prozess Wininit.exe führt die folgenden Systeminitialisierungsfunktionen aus: 1. Er kennzeichnet sich selbst und den Hauptthread als kritisch. Wenn er vorzeitig beendet und das System im Debugmodus gestartet wird, steigt er dadurch in den Debugger ein. (Anderenfalls stürzt das System ab.) 2. Er sorgt dafür, dass der Prozess bestimmte Fehler als kritisch behandelt, z. B. Verwendung ungültiger Handles und Beschädigungen des Heaps. 3. Er initialisiert die Unterstützung für die Statustrennung, falls die SKU dies ermöglicht. Hauptsystemkomponenten Kapitel 2 105
4. Er erstellt das Ereignis Global\FirstLogonCheck (es kann in Process Explorer und WinObj im Verzeichnis \BaseNamedObjects beobachtet werden), das die Windows-Anmeldeprozesse nutzen können, um herauszufinden, welche Anmeldung als Erstes gestartet werden soll. 5. Er erstellt ein WinLogonLogoff-Ereignis im Objekt-Manager-Verzeichnis BaseNamedObjects, das von Winlogon-Instanzen verwendet werden kann. Dieses Ereignis wird gesendet, wenn eine Abmeldeoperation beginnt. 6. Er erhöht seine eigene Basispriorität auf 13 und die des Hauptthreads auf 15. 7. Sofern im Registrierungswert NoDebugThread unter HKLM\Software\Microsoft\Windows NT\ CurrentVersion\Winlogon nichts anderes eingerichtet, erstellt er eine Timerwarteschlange, die nach der Festlegung im Kerneldebugger in jeden Benutzermodusprozess einsteigt. Dadurch können Remotekerneldebugger dafür sorgen, dass sich Winlogon an andere Benutzermodusanwendungen anhängt und darin einsteigt. 8. Er richtet den Computernamen in der Umgebungsvariablen COMPUTERNAME ein und aktualisiert und konfiguriert TCP/IP-Informationen wie den Domänen- und den Hostnamen. 9. Er richtet die Umgebungsvariablen USERPROFILE, ALLUSERPROFILES, PUBLIC und ProgramData für das Standardprofil ein. 10. Er erstellt das temporäre Verzeichnis, indem er %SystemRoot%\Temp erweitert (zum Beispiel C:\Windows\Temp). 11. Er richtet das Laden von Schriftarten und DWM ein, wenn Sitzung 0 interaktiv ist, was von der SKU abhängt. 12. Er erstellt das ursprüngliche Terminal, das aus einer Windows-Station (stets mit dem Namen Winsta0) und zwei Desktops (Winlogon und Default) für Prozesse in Sitzung 0 besteht. 13. Er initialisiert den LSA-Maschinenverschlüsselungsschlüssel in Abhängigkeit davon, ob er lokal gespeichert ist oder interaktiv eingegeben werden muss. Weitere Informationen über die Speicherung lokaler Authentifizierungsschlüssel erhalten Sie in Kapitel 7. 14. Er erstellt den Dienststeuerungs-Manager (Services.exe). Mehr darüber erfahren Sie im folgenden Abschnitt und in Kapitel 9 von Band 2. 15. Er startet den Teilsystemdienst für die lokale Sicherheitsauthentifizierung (Lsass.exe) und, falls CredentialGuard aktiviert ist, auch das isolierte LSA-Trustlet (Lsaiso.exe). Dazu ist es erforderlich, den VBS-Bereitstellungsschlüssel von UEFI abzufragen. Mehr über Lsass.exe und Lsaiso.exe erfahren Sie in Kapitel 7. 16. Wenn Setup aussteht (wenn es sich also um den ersten Start bei einer Neuinstallation oder bei der Aktualisierung auf einen neuen Hauptbuild oder ein Insider-Preview des Betriebssystems handelt), startet er das Setupprogramm. 17. Er wartet unaufhörlich auf eine Anforderung, das System herunterzufahren oder eine der zuvor erwähnten Systemprozesse zu beenden (sofern der Registrierungswert Dont​ WatchSysProcs in dem in Schritt 7 erwähnten Winlogon-Schlüssel nicht gesetzt ist). In jedem Fall fährt er das System herunter. 106 Kapitel 2 Systemarchitektur
Der Dienststeuerungs-Manager Bei einem Dienst kann es sich in Windows um einen Serverprozess oder einen Gerätetreiber handeln. In diesem Abschnitt beschäftigen wir uns mit Diensten, bei denen es sich um Benutzermodusprozesse handelt. Ebenso wie Daemon-Prozesse von Linux können Dienste so eingerichtet werden, dass sie beim Hochfahren des Systems automatisch gestartet werden, ohne dass eine interaktive Anmeldung erforderlich wäre. Sie können auch manuell gestartet werden, etwa in der Verwaltungskonsole Dienste, mit dem Programm Sc.exe und durch Aufrufen der Windows-Funktion StartService. Gewöhnlich interagieren Dienste nicht mit dem angemeldeten Benutzer, wobei dies jedoch unter besonderen Umständen ebenfalls möglich ist. Die meisten Dienste laufen unter besonderen Dienstkonten (z. B. SYSTEM und LOCAL SERVICE), doch einige können auch im selben Sicherheitskontext wie das Konto des angemeldeten Benutzers ausgeführt werden (siehe Kapitel 9 in Band 2). Der Dienststeuerungs-Manager (Service Control Manager, SCM) ist ein besonderer Systemprozess, der das Abbild %SystemRoot%\System32\Services.exe ausführt. Dieses ist dafür verantwortlich, Dienstprozesse zu starten, zu beenden und mit ihnen zu interagieren. Er ist geschützt, weshalb es nicht leicht ist, ihn zu manipulieren. Programme für Dienste sind im Grunde genommen lediglich Windows-Abbilder, die bestimmte Windows-Funktionen aufrufen, um mit dem SCM zu interagieren und beispielsweise den erfolgreichen Start des Dienstes zu regis­ trieren, auf Statusanforderungen zu reagieren oder den Dienst anzuhalten oder zu beenden. Dienste werden in der Registrierung unter HKLM\SYSTEM\CurrentControlSet\Services definiert. Denken Sie daran, dass Dienste drei Namen haben: den Prozessnamen, den Sie im System sehen, den internen Namen in der Registrierung und den Anzeigenamen in der Verwaltungskonsole Dienste. (Allerdings haben nicht alle Dienste einen Anzeigenamen. Wenn das der Fall ist, wird in der Konsole der interne Name angegeben.) Dienste können auch ein Beschreibungsfeld mit weiteren Einzelheiten darüber enthalten, was sie jeweils tun. Um einen Dienstprozess den darin enthaltenen Diensten zuzuordnen, verwenden Sie den Befehl tlist /s (aus den Debugging-Tools für Windows) oder takslist /svc (in Windows enthalten). Beachten Sie jedoch, dass es nicht immer eine 1:1-Zuordnung zwischen Dienstprozessen und laufenden Diensten gibt, da sich manche Dienste einen Prozess teilen. In der Registrierung gibt der Wert Type im Schlüssel des Dienstes an, ob der Dienst in seinem eigenem Prozess läuft oder ihn sich mit anderen Diensten in seinem Abbild teilt. Eine Reihe von Windows-Komponenten sind als Dienste implementiert, z. B. der Druckspooler, das Ereignisprotokoll, der Taskplaner und einige Netzwerkkomponenten. Mehr über Dienste erfahren Sie in Kapitel 9 von Band 2. Hauptsystemkomponenten Kapitel 2 107
Experiment: Die installierten Dienste auflisten Um die installierten Dienste aufzulisten, öffnen Sie die Systemsteuerung, wählen Verwaltung und dann Dienste. Alternativ können Sie auch auf Start klicken und services.msc ausführen. Anschließend sehen Sie eine Ausgabe wie die folgende: Um ausführliche Informationen über einen Dienst einzusehen, rechtsklicken Sie darauf und wählen Eigenschaften. Das folgende Beispiel zeigt die Eigenschaften des Dienstes Windows Update: Das Feld Pfad für ausführbare Datei gibt das Programm an, das diesen Dienst und seine Befehlszeile enthält. Denken Sie daran, dass einige Dienste ihren Prozess mit anderen teilen. Die Zuordnung ist nicht immer 1:1. 108 Kapitel 2 Systemarchitektur
Experiment: Einzelheiten von Diensten in Dienstprozessen einsehen Process Explorer hinterlegt die Prozesse, die einen oder mehr Dienste beherbergen, in Rosa (wobei Sie die Kennzeichnung ändern können, indem Sie das Menü Options öffnen und dort Configure Color wählen). Wenn Sie auf einen Diensthostprozess doppelklicken, wird die Registerkarte Services angezeigt, die alle Dienste in dem Prozess, den Namen des Registrierungsschlüssels, in dem der Dienst definiert ist, den Anzeigenamen, den der Administrator sieht, und die Beschreibung des Dienstes (falls vorhanden) angibt. Bei Svchost. exe-Diensten wird auch der Pfad zu der DLL angezeigt, die den Dienst implementiert. Das folgende Beispiel zeigt die Dienste in einem der Svchost.exe-Prozesse, die unter dem Systemkonto laufen: Winlogon, LogonUI und Userinit Der Windows-Anmeldeprozess (%SystemRoot%\System32\Winlogon.exe) kümmert sich um die interaktiven An- und Abmeldungen der Benutzer. Wenn ein Benutzer die sichere Aufmerksamkeits-Tastenfolge (Secure Attention Sequence, SAS) eingibt, wird Winlogon.exe von einer Benutzeranmeldeanforderung unterrichtet. (Strg) + (Alt) + (Entf) ist die Standard-SAS auf Windows. Der Grund für die Verwendung einer SAS besteht darin, Benutzer vor Programmen zu schützen, die den Anmeldeprozess simulieren, um Passwörter abzugreifen: Benutzermodusanwendungen können diese Tastenfolge nicht abfangen. Die Identifizierungs- und Authentifizierungsaspekte des Anmeldeprozesses werden durch DLLs realisiert, die als Anmeldeinformationsanbieter bezeichnet werden. Die Standard-Anmeldeinformationsanbieter von Windows implementieren die Standard-Authentifizierungsmöglichkeiten von Windows: Passwörter und Smartcards. Windows 10 enthält auch einen biometrischen Anmeldeinformationsanbieter zur Gesichtserkennung, Windows Hello genannt. Hauptsystemkomponenten Kapitel 2 109
Entwickler können jedoch ihre eigenen Anmeldeinformationsanbieter bereitstellen, um andere Identifizierungs- und Authentifizierungsmechanismen als die übliche Abfrage von Name und Kennwort umzusetzen, z. B. über Stimmerkennung oder Fingerabdrücke. Da Winlogon.exe ein kritischer Prozess ist, den das System unbedingt braucht, läuft das Anmeldedialogfeld, das die Anmeldeinformationsanbieter und die grafische Benutzeroberfläche anzeigt, nur in einem Kindprozess von Winlogon.exe, nämlich LogonUI.exe. Wenn Winlogon.exe die SAS erkennt, startet er diesen Prozess, der wiederum die Anmeldeinformationsanbieter initialisiert. Gibt der Benutzer dann seine Anmeldeinformationen (wie von dem jeweiligen Anbieter verlangt) ein oder schließt er die Anmeldeoberfläche, wird der Prozess LogonUI.exe beendet. Winlogon.exe kann auch zusätzliche DLLs für Netzwerkanbieter laden, die eine zweite Authentifizierung erfordern. Dadurch können während der normalen Anmeldung mehrere Netzwerkanbieter gleichzeitig Identifizierungs- und Authentifizierungsinformationen erfassen. Nachdem der Name und das Kennwort (oder andere vom Anmeldeinformationsanbieter geforderten Angaben) erfasst wurden, werden sie zur Authentifizierung an den Prozess Lsass. exe (Local Security Authentication Service; siehe Kapitel 7) gesendet. Lsass.exe ruft die DLL für das entsprechende Authentifizierungspaket auf, um die eigentliche Überprüfung durchzuführen, also um beispielsweise zu prüfen, ob das angegebene Kennwort mit dem übereinstimmt, das in Active Directory oder SAM (dem Teil der Registrierung, der die Definitionen der lokalen Benutzer und Gruppen enthält) gespeichert ist. Bei einer Domänenanmeldung und aktiviertem CredentialGuard kommuniziert Lsass.exe mit dem isolierten LSA-Trustlet (Lsaiso.exe, siehe Kapitel 7), um den Maschinenschlüssel abzurufen, der erforderlich ist, um die Gültigkeit der Authentifizierungsanforderung zu überprüfen. Bei erfolgreicher Authentifizierung ruft Lsass.exe eine Funktion im SRM auf (z. B. NtCreateToken), um ein Zugriffstokenobjekt mit dem Sicherheitsprofil des Benutzers zu erstellen. Wenn die Benutzerkontensteuerung verwendet wird und der Benutzer ein Mitglied der Administratorengruppe ist oder Administratorrechte hat, legt Lsass.exe noch eine zweite, eingeschränkte Version dieses Tokens an. Dieses Token wird anschließend von Winlogon verwendet, um die Anfangsprozesse in der Benutzersitzung zu erstellen. Sie werden im Registrierungswert Userinit unter dem Schlüssel HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon gespeichert. Standardmäßig wird dabei der Prozess Userinit.exe erstellt, aber die Liste kann mehr als ein Abbild enthalten. Userinit.exe führt einige Initialisierungsarbeiten an der Benutzerumgebung aus, indem er beispielsweise das Anmeldeskript ausführt und die Netzwerkverbindungen wiederherstellt. Anschließend untersucht er den Registrierungswert Shell (unter dem zuvor erwähnten Winlogon-Schlüssel) und erstellt einen Prozess, um die vom System definierte Shell auszuführen (standardmäßig Explorer.exe). Danach wird Userinit.exe beendet. Aus diesem Grund wird Explorer ohne Elternprozess angezeigt: Sein Elternprozess wurde beendet. Wie in Kapitel 1 erwähnt, werden Prozesse, deren Elternprozesse nicht mehr laufen, in tlist.exe und Process Explorer linksbündig angezeigt. Explorer ist sozusagen ein Enkelprozess von Winlogon.exe. Winlogon.exe ist nicht nur bei der An- und Abmeldung von Benutzern aktiv, sondern auch immer dann, wenn er die SAS an der Tastatur erfasst. Wenn Sie etwa (Strg) + (Alt) + (Entf) drücken, während Sie angemeldet sind, erscheint der Sicherheitsbildschirm von Windows mit den Optionen zur Abmeldung, zum Starten des Task-Managers, zum Sperren der Arbeits­station, 110 Kapitel 2 Systemarchitektur
zum Herunterfahren des Systems usw. Winlogon.exe und LogonUI.exe sind die Prozesse, die sich um diese Vorgänge kümmern. Eine ausführliche Beschreibung der Schritte, die bei der Anmeldung ablaufen, erhalten Sie in Kapitel 11 von Band 2. Die Authentifizierung wird ausführlicher in Kapitel 7 erklärt. Einzelheiten über die aufrufbaren, mit Lsass.exe gekoppelten Funktionen (deren Namen mit Lsa beginnen) finden Sie in der Dokumentation des Windows SDK. Zusammenfassung Dieses Kapitel hat einen groben Überblick über die Systemarchitektur von Windows gegeben. Sie haben die Schlüsselkomponenten von Windows und deren Beziehungen untereinander kennengelernt. Im nächsten Kapitel werfen wir einen genaueren Blick auf Prozesse. Sie gehören zu den grundlegendsten Dingen in Windows. Zusammenfassung Kapitel 2 111

K apite l 3 Prozesse und Jobs In diesem Kapitel erklären wir die Datenstrukturen und Algorithmen für die Prozesse und Jobs in Windows. Als Erstes sehen wir uns dabei an, wie Prozesse erstellt werden. Anschließend untersuchen wir die internen Strukturen, die einen Prozess ausmachen. Danach werfen wir einen Blick auf geschützte Prozesse und ihre Unterschiede zu nicht geschützten. Wir schauen uns auch die erforderlichen Schritte an, um einen Prozess (und seinen Anfangsthread) zu erstellen. Zum Abschluss des Kapitels befassen wir uns mit Jobs. Da Prozesse so viele Windows-Komponenten berühren, werden in diesem Kapitel eine Reihe von Begriffen und Datenstrukturen erwähnt (wie Arbeitssätze, Threads, Objekte, Handles, Heaps usw.), die an anderen Stellen dieses Buches ausführlich erläutert werden. Um dieses Kapitel richtig verstehen zu können, müssen Sie mit den in Kapitel 1 und 2 vorgestellten Begriffen und Grundprinzipien vertraut sein, also z. B. die Unterschiede zwischen einem Prozess und einem Thread und zwischen Benutzer- und Kernelmodus sowie den Aufbau des virtuellen Adressraums von Windows kennen. Prozesse erstellen Die Windows-API enthält mehrere Funktionen, um Prozesse zu erstellen. Die einfachste ist CreateProcess, die versucht, einen Prozess anzulegen, der dasselbe Zugriffstoken wie der Erstellerprozess hat. Wird ein anderes Token benötigt, bietet sich CreateProcessAsUser an, der als (erstes) zusätzliches Argument ein Handle für ein auf irgendeine Weise (z. B. durch Aufruf der Funktion LogonUser) erworbenes Tokenobjekt entgegennimmt. Weitere Funktionen zum Erstellen von Prozessen sind CreateProcessWithTokenW und CreateProcessWithLogonW (die beide zu Advapi32.dll gehören). CreateProcessWithTokenW ähnelt CreateProcessAsUser, allerdings muss der Aufrufer über andere Rechte verfügen (siehe die Dokumentation des Windows SDK). CreateProcessWithLogonW ist eine praktische Möglichkeit, um sich mit den Anmeldeinformationen eines gegebenen Benutzers anzumelden und gleichzeitig einen Prozess mit dem abgerufenen Token zu erstellen. Um den Prozess tatsächlich zu erstellen, rufen beide Funktionen den Dienst Secondary Logon (seclogon.dll in einem SvcHost.exe-Prozess) über einen Remoteprozeduraufruf auf. SecLogon führt diesen Aufruf in seiner internen Funktion SlrCreateProcessWithLogon aus und ruft, wenn alles gut geht, schließlich CreateProcessAsUser auf. Der Dienst SecLogon ist standardmäßig für den manuellen Start eingerichtet, weshalb er erst beim ersten Aufruf von CreateProcessWithTokenW oder CreateProcessWithLogonW gestartet wird. Schlägt der Dienststart fehl (etwa weil ein Administrator den Dienst deaktiviert hat), so schlagen auch diese Funktionen fehl. Das Befehlszeilenprogramm runas, das Sie wahrscheinlich kennen, nutzt diese Funktionen. Abb. 3–1 stellt die beschriebenen Aufrufe grafisch dar. 113
Prozess erstellen SvcHost.Exe Benutzermodus Kernelmodus Exekutive Abbildung 3–1 Funktionen zum Erstellen eines Prozesses. Interne Funktionen sind durch gestrichelte Kästen gekennzeichnet. Alle zuvor erwähnten, dokumentierten Funktionen erwarten eine ordnungsgemäße PE-Datei (Portable Executive; wobei die Endung .exe jedoch nicht unbedingt erforderlich ist), eine Batchdatei oder eine 16-Bit-COM-Anwendung. Darüber hinaus jedoch wissen sie nicht, welche Zusammenhänge zwischen einzelnen Endungen (wie .txt) und ausführbaren Dateien (wie dem Windows-Editor) bestehen. Solche Kenntnisse werden durch die Windows-Shell in Funktionen wie ShellExecute und ShellExecuteEx bereitgestellt. Sie können beliebige Dateien entgegennehmen (nicht nur ausführbare) und versuchen dann, auf der Grundlage der Dateiendung und der Registrierungseinstellungen in HKEY_CLASSES_ROOT die zugehörige ausführbare Datei zu finden (siehe Kapitel 9 in Band 2). Schließlich ruft ShellExecute(Ex) die Funktion CreateProcess mit einer geeigneten ausführbaren Datei auf und hängt die entsprechenden Argumente an die Befehlszeile an, um das zu erreichen, was der Benutzer tun will (z. B. hängt sie den Dateinamen Notepad.exe an, wenn der Benutzer eine .txt-Datei bearbeiten möchte). Letzten Endes führen all diese Ausführungspfade zu der gemeinsamen internen Funktion CreateProcessInternal, die mit der eigentlichen Arbeit beginnt, um einen Windows-Prozess im Benutzermodus zu erstellen. Wenn alles gut geht, ruft CreateProcessInternal schließlich NtCreateUserProcess in Ntdll.dll auf, um in den Kernelmodus überzugehen und mit den Aspekten der Prozesserstellung weiterzumachen, die im Kernelmodus ablaufen. Dies geschieht in einer Funktion der Exekutive mit demselben Namen (NtCreateUserProcess). Argumente der CreateProcess*-Funktionen Es lohnt sich, einen Blick auf die Argumente der CreateProcess*-Funktionen zu werfen. Einigen davon werden Sie in dem Abschnitt über den Ablauf von CreateProcess wiederbegegnen. Ein Prozess, der im Benutzermodus erstellt wird, enthält immer einen Thread, der schließlich die Hauptfunktion der ausführbaren Datei ausführen wird. Die wichtigsten Argumente für CreateProcess*-Funktionen lauten: 114 Kapitel 3 Prozesse und Jobs
QQ Bei CreateProcessAsUser und CreateProcessWithTokenW das Tokenhandle, unter dem der neue Prozess ausgeführt werden soll. Bei CreateProcessWithLogonW sind der Benutzername, die Domäne und das Kennwort erforderlich. QQ Der Pfad der ausführbaren Datei und die Befehlszeilenargumente. QQ Optionale Sicherheitsattribute für den neuen Prozess und das Threadobjekt. QQ Ein boolesches Flag, das angibt, ob alle als vererbbar gekennzeichneten Handles im aktuellen Prozess (dem Erstellerprozess) vom neuen Prozess geerbt (in ihn kopiert) werden sollen. (Mehr über Handles und die Handlevererbung erfahren Sie in Kapitel 8 von Band 2.) QQ Mehrere Flags, die den Vorgang zum Erstellen des Prozesses steuern. Die Dokumentation des Windows SDK enthält die vollständige Liste. Hier geben wir nur einige Beispiele: • CREATE_SUSPENDED Erstellt den Anfangsthread des neuen Prozesses im angehaltenen Zustand. Der Thread wird ausgeführt, wenn Sie später ResumeThread aufrufen. • DEBUG_PROCESS Der Erstellerprozess deklariert sich selbst als Debugger und erstellt den neuen Prozess unter seiner Kontrolle. • EXTENDED_STARTUPINFO_PRESENT Anstelle von STARTUPINFO wird die erweiterte Struktur STARTUPINFOEX bereitgestellt (siehe weiter unten). QQ Optional ein Umgebungsblock für den neuen Prozess, der die Umgebungsvariablen angibt. Wird er nicht angegeben, so wird er vom Erstellerprozess geerbt. QQ Optional das aktuelle Verzeichnis für den neuen Prozess. Wird es nicht angegeben, so wird es vom Erstellerprozess geerbt. Der neue Prozess kann später SetCurrentDirectory aufrufen, um ein anderes Verzeichnis festzulegen. Das aktuelle Verzeichnis eines Prozesses wird in verschiedenen Suchvorgängen ohne Angabe eines vollständigen Pfades genutzt (z. B. beim Laden einer DLL nur mit dem Dateinamen). QQ Eine STARTUPINFO- oder STARTUPINFOEX-Struktur, die noch mehr Konfigurationseinstellungen für den Erstellungsvorgang eines Prozesses bietet. STARTUPINFOEX enthält ein zusätzliches, undurchsichtiges Feld für einen Satz von Prozess- und Threadattributen, bei denen es sich im Grunde genommen um ein Array aus Schlüssel-Wert-Paaren handelt. Die Attribute werden durch den Aufruf von UpdateProcThreadAttributes für jedes benötigte Attribut festgelegt. Einige von ihnen sind nicht dokumentiert und werden intern verwendet, z. B. beim Erstellen von Store-Apps (siehe den nächsten Abschnitt). QQ Eine PROCESS_INFORMATION-Struktur für die Ausgabe beim erfolgreichen Erstellen des Prozesses. Sie enthält die IDs des neuen Prozesses und des neuen Threads sowie die Handles dafür. Diese Handles kann der Erstellerprozess nutzen, wenn er den neuen Prozess oder Thread anschließend auf irgendeine Weise bearbeiten möchte. Moderne Windows-Prozesse erstellen In Kapitel 1 wurde die neue Art von Anwendungen beschrieben, die ab Windows 8 und Windows Server 2012 zur Verfügung stehen. Die Namen solcher Anwendungen wurden im Laufe der Zeit geändert. Wir nennen sie hier moderne Anwendungen, UWP-Anwendungen oder immersive Prozesse, um sie von den klassischen Desktopanwendungen zu unterscheiden. Prozesse erstellen Kapitel 3 115
Um einen Prozess für eine moderne Anwendung zu erstellen, ist mehr erforderlich, als einfach CreateProcess mit dem Pfad der ausführbaren Datei aufzurufen. Es sind auch einige Befehlszeilenargumente und (mithilfe von UpdateProcThreadAttribute) ein nicht dokumentiertes Prozessattribut mit dem Schlüssel PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME anzugeben. Dieses Attribut ist nicht dokumentiert, aber die API bietet noch andere Möglichkeiten, um eine Store-App auszuführen. Beispielsweise enthält die Windows-API die COM-Schnittstelle IApplicationActivationManager, die von einer COM-Klasse mit der Klassen-ID CLSID_­ ApplicationActivationManager implementiert wird. Eine der Methoden in dieser Schnittstelle ist ActivateApplication, und damit können Sie eine Store-App starten, nachdem Sie mit einem Aufruf von GetPackageApplicationsIDs die sogenannte AppUserModelId aus dem vollständigen Paketnamen der App abgerufen haben. (Mehr über diese APIs erfahren Sie im Windows SDK.) Mehr über Paketnamen und darüber, wie eine Store-App erstellt wird, wenn ein Benutzer auf die Kachel einer modernen App tippt (wobei letzten Endes ebenfalls CreateProcess aufgerufen wird), erfahren Sie in Kapitel 9 von Band 2. Andere Arten von Prozessen erstellen Windows-Anwendungen starten entweder klassische oder moderne Anwendungen, doch die Exekutive bietet auch Unterstützung für andere Arten von Prozessen, die durch Umgehen der Windows-API gestartet werden müssen. Dazu gehören native, minimale und Pico-Prozesse. Beispielsweise ist der in Kapitel 2 vorgestellte Sitzungs-Manager (Smss) ein natives Abbild. Da er direkt vom Kernel erstellt wird, nutzt er offensichtlich nicht die API CreateProcess, sondern ruft direkt NtCreateUserProcess auf. Wenn Smss die Prozesse Autochk (das Programm zur Überprüfung der Festplatte) oder Csrss (den Windows-Teilsystemprozess) erstellt, steht die WindowsAPI gar nicht zur Verfügung, weshalb ebenfalls NtCreateUserProcess verwendet werden muss. Des Weiteren können Windows-Anwendungen keine nativen Prozesse erstellen, da die Funk­ tion CreateProcessInternal Abbilder mit dem Typ des nativen Teilsystems ablehnt. Um all diese Schwierigkeiten abzumildern, enthält die native Bibliothek Ntdll.dll die exportierte Hilfsfunk­ tion RtlCreateUserProcess, die einen einfachen Wrapper um NtCreateUserProcess darstellt. Wie der Name schon sagt, dient NtCreateUserProcess dazu, Benutzermodusprozesse zu erstellen. Wie Sie in Kapitel 2 gesehen haben, enthält Windows jedoch auch eine Reihe von Kernelmodusprozessen wie den System- und den Speicherkomprimierungsprozess (bei denen es sich um minimale Prozesse handelt). Es können auch Pico-Prozesse vorhanden sein, die von einem Anbieter wie dem Windows-Teilsystem für Linux verwaltet werden. Erstellt werden solche Prozesse durch einen Systemaufruf von NtCreateProcessEx, wobei einige Möglichkeiten (etwa das Erstellen von minimalen Prozessen) ausschließlich für Aufrufer aus dem Kernelmodus reserviert sind. Pico-Anbieter rufen die Hilfsfunktion PspCreatePicoProcess auf, die sich sowohl darum kümmert, den minimalen Prozess zu erstellen, als auch dessen Pico-Anbieterkontext zu initialisieren. Diese Funktion wird nicht exportiert und ist nur über eine besondere Schnittstelle ausschließlich für Pico-Anbieter verfügbar. Wie Sie in dem Abschnitt über den Ablauf weiter hinten in diesem Kapitel noch sehen werden, handelt es sich bei NtCreateProcessEx und NtCreateUserProcess um verschiedene System­ aufrufe, die aber auf dieselben internen Routinen zurückgreifen, um ihre Arbeit zu erledigen, 116 Kapitel 3 Prozesse und Jobs
nämlich PspAllocateProcess und PspInsertProcess. Alle Möglichkeiten, um einen Prozess zu erstellen, die Sie bisher betrachtet haben, und auch alle weiteren Möglichkeiten, die Sie sich vorstellen können – von einem WMI-PowerShell-Cmdlet bis zu einem Kerneltreiber – landen schließlich dort. Interne Mechanismen von Prozessen Dieser Abschnitt beschreibt die wichtigsten Datenstrukturen von Windows-Prozessen, die von verschiedenen Teilen des Systems unterhalten werden, und die Möglichkeiten und Werkzeuge zu ihrer Untersuchung. Jeder Windows-Prozess wird durch eine Exekutivprozessstruktur (EPROCESS) dargestellt. Neben vielen Prozessattributen enthält eine EPROCESS-Struktur auch weitere zugehörige Daten­ strukturen bzw. verweist darauf. Beispielsweise hat jeder Prozess einen oder mehrere Threads, die jeweils durch eine Exekutivthreadstruktur (ETHREAD) dargestellt werden. (Threaddatenstrukturen werden in Kapitel 4 erklärt.) Die EPROCESS-Struktur und die meisten ihrer zugehörigen Datenstrukturen befinden sich im Systemadressraum. Eine Ausnahme bildet der Prozessumgebungsblock (Process Environment Block, PEB), der sich im Prozess- (Benutzer-) Adressraum befindet (da er Informationen enthält, auf die Benutzermoduscode zugreifen muss). Außerdem sind einige der Prozessdatenstrukturen für die Speicherverwaltung, z. B. die Liste der Arbeitssätze, nur im Kontext des aktuellen Prozesses gültig, da sie im prozessspezifischen Systemadressraum gespeichert sind. (Mehr über den Prozessadressraum erfahren Sie in Kapitel 5.) Für jeden Prozess, der ein Windows-Programm ausführt, unterhält der Prozess des Windows-Teilsystems (Csrss) eine parallele Struktur namens CSR_PROCESS und der Kernelmodusteil des Windows-Teilsystems (Win32k.sys) die Datenstruktur W32PROCESS, die erstellt wird, wenn ein Thread zum ersten Mal die im Kernelmodus implementierte Windows-Funktion USER oder GDI aufruft. Das geschieht, sobald die Bibliothek User32.dll geladen ist. Typische Funktionen, die das Laden dieser Bibliothek veranlassen, sind CreateWindow(Ex) und GetMessage. Da der Kernelmodusteil des Windows-Teilsystems starken Gebrauch von Grafiken mit DirectX-Hardwarebeschleunigung macht, veranlasst die GDI-Infrastruktur (Graphics Device Interface) den DirectX-Grafikkernel (Dxgkrnl.sys), eine eigene Struktur zu initialisieren, nämlich DXGPROCESS. Sie enthält Informationen für DirectX-Objekte (Oberflächen, Schattierer usw.) und die GPGPU-Zähler und Richtlinieneinstellungen für die Zeitplanung sowohl für Rechenvorgänge als auch für die Speicherverwaltung. Außerdem wird beim Leerlaufprozess jede EPROCESS-Struktur vom Objekt-Manager der Exekutive als ein Prozessobjekt gekapselt (siehe Kapitel 8 von Band 2). Da Prozesse keine benannten Objekte sind, werden sie im Sysinternals-Programm WinObj nicht angezeigt. Sie können sich in WinObj jedoch das Type-Objekt Process im Verzeichnis \ObjectTypes ansehen. Ein Handle für einen Prozess ermöglicht durch die Nutzung der APIs für diesen Prozess den Zugriff auf Daten in der EPROCESS-Struktur und einiger ihrer zugehörigen Strukturen. Durch die Registrierung von Prozesserstellungsbenachrichtigungen können viele andere Treiber und Systemkomponenten ihre eigenen Datenstrukturen erstellen, um die Informa­ tionen, die sie prozessweise speichern, zu verfolgen. (Das ist durch die Exekutivfunktionen Interne Mechanismen von Prozessen Kapitel 3 117
P­ sSetCreateProcessNotifyRoutine(Ex, Ex2) möglich, die im WDK dokumentiert sind.) Um den Prozessoverhead zu bestimmen, muss die Größe dieser Datenstrukturen oft berücksichtigt werden, wobei es jedoch praktisch unmöglich ist, genaue Zahlen zu ermitteln. Außerdem erlauben einige dieser Funktionen solchen Komponenten, das Erstellen von Prozessen zu verbieten oder zu blockieren. Dadurch erhalten Antimalware-Anbieter in der Architektur eine Möglichkeit, Sicherheitsverbesserungen am Betriebssystem vorzunehmen, z. B. durch hashgestützte Blacklists oder andere Techniken. Sehen wir uns als Erstes das Prozessobjekt an. Abb. 3–2 zeigt die wichtigsten Felder in einer EProcess-Struktur. Prozesssteuerungsblock (PCB) Schutzsperren ID des Prozesses und des Elternprozesses PsActiveProcessHead Link zum aktiven Prozess Prozessflags Erstellungs- und Beendigungszeit des Prozesses Kontingentinformationen Sitzungsobjekt Sitzungsprozesslink Sicherheits-/Ausnahme-/Debug-Ports Primäres Zugriffstoken Job, zu dem der Prozess gehört Handletabelle Prozessumgebungsblock (PEB) Abbilddateiname Abbildbasisadresse Prozesszähler (Speicher, E/A ...) Threadlistenkopf Laufende Nummer Win32k-Prozessstruktur Wow64-Prozessstruktur (nur 64 Bit) Trustlet-Identität (Win 10 x64) Stromverbrauchswerte (Win 10) Pico-Kontext (Win 10) Schutzebene und Signatur DirectX-Prozess Arbeitssatz Abschnittsobjekt Abbildung 3–2 Wichtige Felder in der EPROCESS-Struktur Die Datenstrukturen für einen Prozess folgen einem ähnlichen Aufbau wie die APIs und Komponenten des Kernels, die auf isolierte und geschichtete Module mit ihren eigenen Namenskonventionen verteilt sind. Wie Abb. 3–2 zeigt, ist das erste Element der EPROCESS-Struktur der Prozesssteuerungsblock (Process Control Block, PCB). Dabei handelt es sich um eine Struktur vom Typ KPROCESS (Kernelprozess). Routinen in der Exekutive speichern Informationen in der EPROCESS-Struktur, doch der Dispatcher, der Scheduler und der Interrupt- und Zeiterfassungscode, die zum Betriebssystemkernel gehören, verwenden stattdessen die KPROCESS-Struktur. Dies ermöglicht eine Abstraktionsebene zwischen der höheren Funktionalität der Exekutive und den zugrunde liegenden Implementierungen einzelner Funktionen und hilft, unerwünschte Abhängigkeiten zwischen den Ebenen zu vermeiden. Abb. 3–3 zeigt die wichtigsten Felder einer KPROCESS-Struktur. 118 Kapitel 3 Prozesse und Jobs
Dispatcher-Header Stamm des Seitenverzeichnisses für den Prozess Seitenverzeichnis Kernelzeit Benutzerzeit Zykluszeit Kontextwechsel Threadlistenkopf KTHREAD Prozesssperre Prozessaffinität Prozessflags Idealer Knoten Thread-Seed Basispriorität KPROCESS Prozesslisteneintrag KPROCESS Instrumentierungs-Callback Sichere Prozess-ID Abbildung 3–3 Wichtige Felder der Kernelprozessstruktur Experiment: Das Format einer EPROCESS-Struktur anzeigen Um sich eine Liste der Felder einer EPROCESS und ihrer Offsets im Hexadezimalformat anzusehen, geben Sie im Kerneldebugger dt nt!_eprocess ein. (Mehr über den Kerneldebugger und seine Verwendung für das Kerneldebugging auf dem lokalen System erfahren Sie in Kapitel 1.) Das folgende Listing zeigt eine Ausgabe auf einem 64-Bit-System mit Windows 10, die allerdings aus Platzgründen gekürzt wurde: lkd> dt nt!_eprocess +0x000 Pcb : _KPROCESS +0x2d8 ProcessLock : _EX_PUSH_LOCK +0x2e0 RundownProtect : _EX_RUNDOWN_REF +0x2e8 UniqueProcessId : Ptr64 Void +0x2f0 ActiveProcessLinks : _LIST_ENTRY ... +0x3a8 Win32Process : Ptr64 Void +0x3b0 Job : Ptr64 _EJOB ... +0x418 ObjectTable : Ptr64 _HANDLE_TABLE +0x420 DebugPort : Ptr64 Void +0x428 WoW64Process : Ptr64 _EWOW64PROCESS ... +0x758 SharedCommitCharge : Uint8B +0x760 SharedCommitLock : _EX_PUSH_LOCK → Interne Mechanismen von Prozessen Kapitel 3 119
+0x768 SharedCommitLinks : _LIST_ENTRY +0x778 AllowedCpuSets : Uint8B +0x780 DefaultCpuSets : Uint8B +0x778 AllowedCpuSetsIndirect : Ptr64 Uint8B +0x780 DefaultCpuSetsIndirect : Ptr64 Uint8B Das erste Element dieser Struktur (Pcb) ist eine eingebettete KPROCESS-Struktur. Dort sind die Scheduling- und Zeiterfassungsdaten gespeichert. Das Format der KPROCESS-Struktur können Sie sich auf die gleiche Weise anzeigen lassen wie das der EPROCESS-Struktur: lkd> dt nt!_kprocess +0x000 Header : _DISPATCHER_HEADER +0x018 ProfileListHead : _LIST_ENTRY +0x028 DirectoryTableBase : Uint8B +0x030 ThreadListHead : _LIST_ENTRY +0x040 ProcessLock : Uint4B ... +0x26c KernelTime : Uint4B +0x270 UserTime : Uint4B +0x274 LdtFreeSelectorHint : Uint2B +0x276 LdtTableLength : Uint2B +0x278 LdtSystemDescriptor : _KGDTENTRY64 +0x288 LdtBaseAddress : Ptr64 Void +0x290 LdtProcessLock : _FAST_MUTEX +0x2c8 InstrumentationCallback : Ptr64 Void +0x2d0 SecurePid : Uint8B Mit dem Befehl dt können Sie sich auch den Inhalt eines oder mehrere Felder anzeigen lassen, indem Sie hinter dem Strukturnamen den Feldnamen angeben. Beispielsweise zeigt dt nt!_eprocess UniqueProcessId das Feld mit der eindeutigen Prozess-ID an. Wenn ein Feld wiederum für eine Struktur steht – wie das Feld Pcb in der EPROCESS-Struktur, das selbst eine KPROCESS-Unterstruktur enthält –, können Sie hinter dem Feldnamen einen Punkt angeben, um den Debugger anzuweisen, die Unterstruktur anzuzeigen. Wenn Sie also beispielsweise die KPROCESS-Struktur ansehen wollen, geben Sie dt nt!_eprocess Pcb. ein. Dahinter können Sie wiederum die Namen von Feldern (innerhalb der KPROCESS-Struktur) angeben usw. Mit dem Schalter -r des Befehls dt können Sie sämtliche Unterstrukturen durchlaufen. Wenn Sie hinter dem Schalter eine Zahl angeben, legen Sie damit die Verschachtelungstiefe vor, bis zu der sich der Befehl vorarbeitet. In der zuvor gezeigten Verwendung zeigt der Befehl dt jedoch das Format der ausgewählten Struktur und nicht die Inhalte irgendwelcher Instanzen dieses Strukturtyps an. Um die Instanz eines Prozesses zu sehen, geben Sie die Adresse einer EPROCESS-Struktur als Argument von dt an. Die Adressen fast aller EPROCESS-Strukturen im System (außer für den Leerlaufprozess) können Sie durch den Befehl !process 0 0 abrufen. Da die KPROCESS-Struktur das erste Element der EPROCESS-Struktur ist, können Sie bei dt _kprocess als Adresse der KPROCESS-Struktur auch die Adresse der EPROCESS-Struktur angeben. 120 Kapitel 3 Prozesse und Jobs
Experiment: Der Befehl !process des Kerneldebuggers Der Befehl !process des Kerneldebuggers zeigt einen Teil der Informationen in einem Prozessobjekt und dessen zugehörigen Strukturen an. Die Ausgabe für einen Prozess besteht jeweils aus zwei Teilen. Als Erstes sehen Sie wie im Folgenden Angaben über den Prozess. Wenn Sie keine Prozessadresse oder -ID angeben, gibt !process Informationen über den Prozess aus, dem der zurzeit auf CPU 0 ausgeführte Thread gehört. Auf einem Einzelprozessorsystem kann das auch der WinDbg-Prozess selbst sein (bzw. livekd, falls Sie diesen Debugger statt WinDbg verwenden). lkd> !process PROCESS ffffe0011c3243c0 SessionId: 2 Cid: 0e38 Peb: 5f2f1de000 ParentCid: 0f08 DirBase: 38b3e000 ObjectTable: ffffc000a2b22200 HandleCount: <Data Not Accessible> Image: windbg.exe VadRoot ffffe0011badae60 Vads 117 Clone 0 Private 3563. Modified 228. Locked 1. DeviceMap ffffc000984e4330 Token ffffc000a13f39a0 ElapsedTime 00:00:20.772 UserTime 00:00:00.000 KernelTime 00:00:00.015 QuotaPoolUsage[PagedPool] 299512 QuotaPoolUsage[NonPagedPool] 16240 Working Set Sizes (now,min,max) (9719, 50, 345) (38876KB, 200KB, 1380KB) PeakWorkingSetSize 9947 VirtualSize 2097319 Mb PeakVirtualSize 2097321 Mb PageFaultCount 13603 MemoryPriority FOREGROUND BasePriority 8 CommitCharge 3994 Job ffffe0011b853690 Auf diese Ausgabe von grundlegenden Prozessinformationen folgt eine Liste der Threads in dem Prozess. Diese Ausgabe wird im Experiment »Der Befehl !thread des Kerneldebuggers« in Kapitel 4 erläutert. Es gibt noch weitere Befehle, die Prozessinformationen anzeigen. Dazu gehört auch !handle, der die Prozesshandletabelle ausgibt (siehe Kapitel 8 von Band 2). Sicherheitsstrukturen von Prozessen und Threads sind Thema von Kapitel 7. → Interne Mechanismen von Prozessen Kapitel 3 121
In dieser Ausgabe erhalten Sie auch die Adresse des PEB. Sie können sie im Befehl !peb einsetzen, der im nächsten Experiment beschrieben wird, um sich den PEB eines beliebigen Prozesses auf übersichtliche Weise anzeigen zu lassen. Es ist auch möglich, den regulären Befehl dt mit der _PEB-Struktur zu verwenden. Da sich der PEB im Benutzeradressraum befindet, ist er jedoch nur im Kontext seines eigenen Prozesses gültig. Um sich den PEB eines anderen Prozesses anzusehen, müssen Sie in WinDbg erst zu diesem Prozess wechseln. Das können Sie mit dem Befehl .process /P gefolgt von dem EPROCESS-Zeiger tun. Die Version von WinDbg im aktuellsten Windows 10 SDK zeigt unterhalb der PEB-Adresse einen Hyperlink an, über den Sie die Befehle .process und !peb automatisch ausführen lassen können. Der PEB befindet sich im Benutzeradressraum des Prozesses, den er beschreibt. Er enthält Informationen, die der Abbildlader, der Heap-Manager und andere Windows-Komponenten benötigen, um vom Benutzermodus aus auf den Prozess zuzugreifen. Es wäre zu aufwendig, all diese Informationen über Systemaufrufe verfügbar zu machen. Die EPROCESS- und die KPROCESS-Struktur sind nur vom Kernelmodus aus zugänglich. Die wichtigsten Felder des PEB sehen Sie in Abb. 3–4 Eine ausführliche Erklärung folgt weiter hinten in diesem Kapitel. Basisadresse des Abbilds Laderdatenbank Lokale Threadspeicherdaten Prozessflags Prozesserstellungsparameter Prozessheap (Zeiger) Prozessheap Globale NT-Flags Gemein genutzte GDI-Handletabelle (Zeiger) Anzahl Prozessoren Prozessheapinformationen Ladersperre Angabe der Betriebssytemversion AppCompat-Daten Shim-Daten Sitzungs-ID Lokale Fiberspeicherdaten Assembly- und Aktivierungskontext Gemeinsam genutzter CSRSS-Speicher (Zeiger) Threadpoolinformationen Abbildung 3–4 Wichtige Felder des Prozessumgebungsblocks 122 Kapitel 3 Prozesse und Jobs Gemein genutzte GDI-Handletabelle
Experiment: Den PEB untersuchen Mit dem Befehl !peb des Kerneldebuggers können Sie die PEB-Struktur des Prozesses ausgeben, dem der zurzeit auf CPU 0 ausgeführte Thread gehört. Sie können auch den im vorherigen Experiment gewonnenen PEB-Zeiger als Argument für den Befehl angeben. lkd> .process /P ffffe0011c3243c0 ; !peb 5f2f1de000 PEB at 0000003561545000 InheritedAddressSpace: No ReadImageFileExecOptions: No BeingDebugged: No ImageBaseAddress: 00007ff64fa70000 Ldr 00007ffdf52f5200 Ldr.Initialized: Yes Ldr.InInitializationOrderModuleList: 000001d3d22b3630 . 000001d3d6cddb60 Ldr.InLoadOrderModuleList: 000001d3d22b3790 . 000001d3d6cddb40 Ldr.InMemoryOrderModuleList: 000001d3d22b37a0 . 000001d3d6cddb50 Base TimeStamp Module 7ff64fa70000 56ccafdd Feb 23 21:15:41 2016 C:\dbg\x64\windbg.exe 7ffdf51b0000 56cbf9dd Feb 23 08:19:09 2016 C:\WINDOWS\SYSTEM32\ntdll.dll 7ffdf2c10000 5632d5aa Oct 30 04:27:54 2015 C:\WINDOWS\system32\KERNEL32.DLL ... Die CSR_PROCESS-Struktur enthält spezifische Prozessinformationen des Windows-Teilsystems (Csrss). Daher ist eine solche Struktur nur mit Windows-Anwendungen verbunden. (Beispielsweise hat Smss keine.) Da jede Sitzung ihre eigene Instanz des Windows-Teilsystems aufweist, werden die CSR_PROCESS-Strukturen jeweils vom Csrss-Prozess der Sitzung gepflegt. Den grundlegenden Aufbau einer SCR_PROCESS-Struktur sehen Sie in Abb. 3–5. Eine ausführliche Erklärung folgt weiter hinten in diesem Kapitel. Interne Mechanismen von Prozessen Kapitel 3 123
Client-ID Prozesslink Threadlistenkopf Clientporthandle Prozesshandle Clientsichtdaten Sitzungsdaten Prozessflags Referenzzähler Nachrichtendaten Shutdown-Ebene Eines pro CSR_SERVER_DLL Flags Prozessgruppendaten Serverdaten Abbildung 3–5 Felder einer CSR_PROCESS-Struktur Experiment: Die CSR_PROCESS-Struktur untersuchen Csrss-Prozesse sind geschützt (mehr darüber weiter hinten in diesem Kapitel), weshalb es nicht möglich ist, einen Benutzermodusdebugger daran anzuhängen (nicht einmal mit erhöhten Rechten oder auf nicht invasive Weise). Stattdessen verwenden wir den Kerneldebugger. Als Erstes listen wir die Csrss-Prozesse auf: lkd> !process 0 0 csrss.exe PROCESS ffffe00077ddf080 SessionId: 0 Cid: 02c0 Peb: c4e3fc0000 ParentCid: 026c DirBase: ObjectTable: ffffc0004d15d040 HandleCount: 543. Image: csrss.exe PROCESS ffffe00078796080 SessionId: 1 Cid: 0338 Peb: d4b4db4000 ParentCid: 0330 DirBase: ObjectTable: ffffc0004ddff040 HandleCount: 514. Image: csrss.exe Als Nächstes ändern wir den Debuggerkontext und lassen ihn auf einen dieser Prozesse verweisen, sodass die Benutzermodusmodule sichtbar werden: lkd> .process /r /P ffffe00078796080 Implicit process is now ffffe000'78796080 Loading User Symbols ............. → 124 Kapitel 3 Prozesse und Jobs
Der Schalter /p ändert den Prozesskontext des Debuggers auf den des angegebenen Prozessobjekts (EPROCESS, was hauptsächlich beim Live-Debugging benötigt wird), und /r fordert, die Benutzermodussymbole zu laden. Anschließend können Sie sich die Module mit dem Befehl lm ansehen oder die CSR_PROCESS-Struktur betrachten: lkd> dt csrss!_csr_process +0x000 ClientId : _CLIENT_ID +0x010 ListLink : _LIST_ENTRY +0x020 ThreadList : _LIST_ENTRY +0x030 NtSession : Ptr64 _CSR_NT_SESSION +0x038 ClientPort : Ptr64 Void +0x040 ClientViewBase : Ptr64 Char +0x048 ClientViewBounds : Ptr64 Char +0x050 ProcessHandle : Ptr64 Void +0x058 SequenceNumber : Uint4B +0x05c Flags : Uint4B +0x060 DebugFlags : Uint4B +0x064 ReferenceCount : Int4B +0x068 ProcessGroupId : Uint4B +0x06c ProcessGroupSequence : Uint4B +0x070 LastMessageSequence : Uint4B +0x074 NumOutstandingMessages : Uint4B +0x078 ShutdownLevel : Uint4B +0x07c ShutdownFlags : Uint4B +0x080 Luid : _LUID +0x088 ServerDllPerProcessData : [1] Ptr64 Void W32PROCESS ist die letzte Systemdatenstruktur im Zusammenhang mit Prozessen, die wir uns ansehen wollen. Sie enthält alle Informationen, die der Grafik- und Fensterverwaltungscode im Windows-Kernel (Win32k) benötigt, um die Statusinformationen über die GUI-Prozesse zu unterhalten (die weiter vorn als Prozesse mit mindestens einem USER/GDI-Systemaufruf definiert worden sind). Den grundlegenden Aufbau einer W32PROCESS-Struktur sehen Sie in Abb. 3–6. Da Typinformationen für Win32k-Strukturen in öffentlichen Symbolen nicht verfügbar sind, können wir Ihnen leider kein einfaches Experiment zeigen, mit dem Sie sich diese Informationen selbst ansehen könnten. Allerdings ginge eine Erörterung von Datenstrukturen und Funktionsprinzipien für die Grafik ohnehin über den Rahmen dieses Buches hinaus. Interne Mechanismen von Prozessen Kapitel 3 125
Prozess Referenzzähler Flags Prozess-ID GDI-Handlezähler und -Spitzenwert USER-Handlezähler und -Spitzenwert Prozesslink GDI-Listen DirectX-Prozessdaten DXGPROCESS (Singleton) DirectComposition-Prozessdaten Abbildung 3–6 Felder der W32PROCESS-Struktur Geschützte Prozesse Im Windows-Sicherheitsmodell kann jeder Prozess, der mit einem Token mit Debugrechten ausgeführt wird (z. B. unter einem Administratorkonto), jegliche Zugriffsrechte für beliebige andere Prozesse auf demselben Computer anfordern. Beispielsweise kann er willkürlich im Prozessspeicher lesen und schreiben, Code injizieren, Threads anhalten und wieder aufnehmen und Informationen über andere Prozesse abfragen. Werkzeuge wie Process Explorer und der Task-Manager benötigen solche Zugriffsrechte, um ihre Aufgabe zu erfüllen. Dieses Verhalten ist sinnvoll, da es dafür sorgt, dass Administratoren stets die volle Kon­ trolle über die Codeausführung in dem System haben, doch es kollidiert mit dem Systemverhalten für digitale Rechteverwaltung, das die Medienbranche für Computerbetriebssysteme zur Wiedergabe von modernen, hochwertigen digitalen Inhalten wie Blu-Ray-Medien fordert. Um eine zuverlässige und geschützte Wiedergabe solcher Inhalte zu ermöglichen, wurden in Windows Vista und Windows Server 2008 geschützte Prozesse eingeführt. Sie existieren neben den normalen Windows-Prozessen und schränken die Zugriffsrechte, die andere Prozesse auf dem System anfordern können (selbst diejenigen mit Administratorrechten), erheblich ein. Jede Anwendung kann geschützte Prozesse erstellen, allerdings lässt das Betriebssystem den Schutz des Prozesses nur dann zu, wenn die Abbilddatei mit einem besonderen Windows-Medienzertifikat signiert wurde. Der Pfad für geschützte Medien (Protected Media Path, PMP) in Windows nutzt geschützte Prozesse, um wertvolle Medien zu schützen. Entwickler von Anwendungen wie DVD-Playern können über die Media-Foundation-API (MF) geschützte Prozesse verwenden. Der Prozess Audio Device Graph (Audiodg.exe) ist geschützt, da über ihn geschützte Musikinhalte decodiert werden können. Damit verwandt ist die Media Foundation Protected Pipeline (Mfpmp.exe), die aus ähnlichen Gründen ein geschützter Prozess ist. (Er wird nicht standardmäßig ausgeführt.) Der Clientprozess Werfaultsecure.exe der Windows-Fehlerberichterstattung (Windows Error Reporting, WER; siehe Kapitel 8 in Band 2) kann ebenfalls geschützt ausgeführt 126 Kapitel 3 Prozesse und Jobs
werden, da er für den Fall, dass ein geschützter Prozess ausfällt, Zugriff darauf haben muss. Auch der Systemprozess selbst ist geschützt, da einige der Entschlüsselungsinformationen vom Treiber Ksecdd.sys generiert und im Benutzermodusspeicher abgelegt werden. Der Schutz des Systemprozesses erfolgt auch, um die Integrität aller Kernelhandles zu wahren (da die Handletabelle des Systemprozesses alle Kernelhandles des Systems enthält). Dass andere Treiber unter Umständen Arbeitsspeicher im Benutzermodus-Adressraum des Systemprozesses zuordnen können (z. B. das Codeintegritätszertifikat und Katalogdaten), ist ein weiterer Grund, den Prozess zu schützen. Auf Kernelebene gibt es zwei Formen der Unterstützung für geschützte Prozesse. Erstens erfolgt der Großteil der Prozesserstellung im Kernelmodul, um Injektionsangriffe zu vermeiden. (Der Ablauf der Erstellung von normalen und von geschützten Prozessen wird ausführlich im nächsten Abschnitt beschrieben.) Zweitens ist in der EPROCESS-Struktur von geschützten Prozessen (und ihren nächsten Verwandten, den im nächsten Abschnitt beschriebenen PPLs [Protected Processes Light]) ein besonderes Bit gesetzt, das das Verhalten von sicherheitsbezogenen Routinen im Prozess-Manager so ändert, dass bestimmte Zugriffsrechte, die Administratoren normalerweise erhalten, verweigert werden. Die einzigen Zugriffsrechte, die für geschützte Prozesse vergeben werden, sind PROCESS_QUERY/SET_LIMITED_INFORMATION, PROCESS_TERMINATE und PROCESS_SUSPEND_RESUME. Manche Zugriffsrechte werden auch für die Threads innerhalb der geschützten Prozesse deaktiviert. Das sehen wir uns in Kapitel 4 im Abschnitt »Interne Strukturen von Threads« genauer an. Da das Programm Process Explorer reguläre Windows-APIs des Benutzermodus verwendet, um Informationen über Prozessinterna abzurufen, kann es bei geschützten Prozessen bestimmte Operationen nicht durchführen. Dagegen läuft WinDbg im Kerneldebuggingmodus und nutzt daher die Kernelmodus-Infrastruktur, um Informationen zu gewinnen, weshalb es damit möglich ist, sämtliche Informationen anzuzeigen. Das Verhalten von Process Explorer beim Umgang mit einem geschützten Prozess wie Audiodg.exe lernen Sie in dem Experiment im Abschnitt »Innere Mechanismen von Threads« von Kapitel 4 kennen. Wie bereits in Kapitel 1 erwähnt, müssen Sie zum lokalen Kerneldebugging im Debuggingmodus starten (indem Sie bcdedit /debug oder die erweiterten Bootoptionen in Msconfig verwenden). Das dient dazu, die Gefahr von debuggergestützten Angriffen auf geschützte Prozesse und den PMP zu verringern. Beim Start im Debuggingmode können High-Definition-Inhalte nicht wiedergegeben werden. Durch die Einschränkung der Zugriffsrechte kann der Kernel geschützte Prozesse wie in einer Sandbox zuverlässig gegen Zugriff vom Benutzermodus abschirmen. Allerdings sind geschützte Prozesse durch ein Flag in der EPROCESS-Struktur gekennzeichnet, sodass Administratoren immer noch in der Lage sind, einen Kernelmodustreiber zu laden, der dieses Flag ändert. Das allerdings würde eine Verletzung des PMP-Modells darstellen und als schädliches Verhalten eingestuft, weshalb das Laden eines solchen Treibers auf einem 64-Bit-System wahrscheinlich blockiert würde, da die Codesignierungsrichtlinie für den Kernelmodus eine digitale Signierung von Schadcode nicht zulässt. Außerdem würden der Kernelmoduspatchschutz (PatchGuard, siehe Kapitel 7) sowie der Treiber für geschützte Umgebung und Authentifizierung (Protected Geschützte Prozesse Kapitel 3 127
Environment and Authentication, Peauth.sys) solche Versuche erkennen und melden. Selbst auf 32-Bit-Systemen muss der Treiber von der PMP-Richtlinie anerkannt werden, da die Wiedergabe sonst angehalten wird. Diese Richtlinie wird von Microsoft implementiert und nicht von irgendeiner Kernelerkennung. Dieser Block würde ein manuelles Eingreifen von Microsoft erfordern, um die Signatur als bösartig zu erkennen und den Kernel zu aktualisieren. Protected Process Light (PPL) Wie Sie gerade gesehen haben, ging es beim ursprünglichen Modell für geschützte Prozesse vor allem um DRM-geschützte Inhalte. Ab Windows 8.1 und Windows Server 2012 R2 wurde das Modell jedoch um Protected Process Light (PPL), also sozusagen die »Light-Version« von geschützten Prozessen erweitert. PPLs werden auf die gleiche Weise geschützt wie herkömmliche geschützte Prozesse: Benutzermoduscode (selbst solcher mit erhöhten Rechten) kann nicht in sie eindringen und Threads injizieren oder Informationen über die geladenen DLLs gewinnen. Beim PPL-Modell kommen jedoch noch Attributwerte hinzu. Die verschiedenen Signierer haben unterschiedliche Vertrauensebenen, was dazu führt, dass manche PPLs stärker geschützt sind als andere. Da sich die digitale Rechteverwaltung von der reinen Multimedia-DRM auch zur WindowsLizenz-DRM und Windows-Store-DRM entwickelt hat, werden herkömmliche geschützte Prozesse jetzt auch anhand ihres Signiererwerts unterschieden. Die einzelnen anerkannten Signierer legen auch die Zugriffsrechte fest, die weniger geschützten Prozessen verweigert werden. Beispielsweise sind normalerweise nur die Zugriffsmasken PROCESS_QUERY/SET_LIMITED_INFORMATION und PROCESS_SUSPEND_RESUME zugelassen. Dagegen ist PROCESS_TERMINATE für bestimmte PPL-Signierer nicht zugelassen. Tabelle 3–1 zeigt die zulässigen Werte für das Schutzflag in der EPROCESS-Struktur. Internes Symbol für die Schutzebene des Prozesses Schutztyp Signierer PS_PROTECTED_SYSTEM (0x72) Geschützt WinSystem PS_PROTECTED_WINTCB (0x62) Geschützt WinTcb PS_PROTECTED_WINTCB_LIGHT (0x61) PPL WinTcb PS_PROTECTED_WINDOWS (0x52) Geschützt Windows PS_PROTECTED_WINDOWS_LIGHT (0x51) PPL Windows PS_PROTECTED_LSA_LIGHT (0x41) PPL Lsa PS_PROTECTED_ANTIMALWARE_LIGHT (0x31) PPL Anti-malware PS_PROTECTED_AUTHENTICODE (0x21) Geschützt Authenticode PS_PROTECTED_AUTHENTICODE_LIGHT (0x11) PPL Authenticode PS_PROTECTED_NONE (0x00) Keine Keine Tabelle 3–1 Gültige Schutzwerte für Prozesse Wie Sie in Tabelle 3–1 sehen, sind mehrere Signierer definiert, hier in der Reihenfolge absteigender Priorität angegeben. WinSystem ist der Signierer höchster Priorität und wird vom Systemprozess sowie von minimalen Prozessen wie dem Speicherkomprimierungsprozess ver- 128 Kapitel 3 Prozesse und Jobs
wendet. Für Benutzermodusprozesse hat WinTCB (Windows Trusted Computer Base) die höchste Priorität und wird zum Schutz kritischer Prozesse genutzt, die der Kernel genau kennt und für die er seine Sicherheitsmaßnahmen reduzieren mag. Geschützte Prozesse haben stets mehr Befugnisse als PPLs, Prozesse mit einem höherwertigen Signierer haben Zugriff auf Prozesse mit einem Signierer geringerer Priorität, aber nicht umgekehrt. Tabelle 3–2 zeigt die Signierer­ ebenen ( je höher der Wert, umso mehr Befugnisse hat der Signierer) und einige Beispiele ihrer Nutzung. Sie können sie im Debugger mit dem Typ _PS_PROTECTED_SIGNER ausgeben. Signierer (PS_PROTECTED_SIGNER) Ebene Verwendung PsProtectedSignerWinSystem 7 System- und minimale Prozesse (einschließlich Pico-Prozesse) PsProtectedSignerWinTcb 6 Kritische Windows-Komponenten. PROCESS_TERMINATE wird verweigert. PsProtectedSignerWindows 5 Wichtige Windows-Komponenten, die mit sensiblen Daten umgehen PsProtectedSignerLsa 4 Lsass.exe (falls zur geschützten Ausführung eingerichtet) PsProtectedSignerAntimalware 3 Anti-Malware-Dienste und Prozesse, auch von Dritt­ anbietern. PROCESS_TERMINATE wird verweigert. PsProtectedSignerCodeGen 2 NGEN (native .NET-Codegenerierung) PsProtectedSignerAuthenticode 1 Hosting von DRM-Inhalten und Laden von Benutzermodus-Schriftarten PsProtectedSignerNone 0 Ungültig (kein Schutz) Tabelle 3–2 Signierer und ihre Ebenen Vielleicht fragen Sie sich an dieser Stelle, was einen Schadprozess daran hindert, sich als geschützten Prozess auszugeben und sich gegen Anti-Malware-Anwendungen abzuschirmen. Da für eine Ausführung als geschützter Prozess das Windows-Media-DRM-Zertifikat nicht mehr erforderlich ist, hat Microsoft sein Codeintegritätsmodul so erweitert, dass es die beiden besonderen EKU-OIDs (Enhanced Key Usage) 1.3.6.1.4.1.311.10.3.22 und 1.3.6.4.1.311.10.3.20 versteht, die in Zertifikaten zur Codesignierung verwendet werden können. Ist eine dieser EKUs vorhanden, werden die im Zertifikat hartcodierten Strings für den Signierer und den Aussteller zusammen mit weiteren möglichen EKUs mit den verschiedenen Werten für geschützte Signierer verknüpft. Beispielsweise kann der Herausgeber Microsoft Windows den geschützten Signiererwert PsProtectedSignerWindows gewähren, aber nur dann, wenn die EKU für Windows System Component Verification (1.3.6.1.4.1.311.10.3.6) ebenfalls vorhanden ist. Als Beispiel zeigt Abb. 3–7 das Zertifikat für Smss.exe, der als WinTcb-Light ausgeführt werden darf. Die Schutzebene eines Prozesses bestimmt auch, welche DLLs er laden darf, denn ansonsten kann ein legitimer geschützter Prozess durch einen Bug oder eine einfache Dateiersetzung dazu gebracht werden, eine Drittanbieter- oder eine Schadbibliothek zu laden, die dann mit derselben Schutzebene ausgeführt würde wie der Prozess. Dazu erhält jeder Prozess eine »Signaturebene«, die im Feld SignatureLevel der EPROCESS-Struktur gespeichert wird. In einer internen Nachschlagetabelle wird dann die zugehörige DLL-Signaturebene gesucht, die als SectionSignatureLevel in der EPROCESS-Struktur steht. Jede in den Prozess geladene DLL wird Geschützte Prozesse Kapitel 3 129
von der Codeintegritätskomponente auf diese Weise überprüft wie die ausführbare Hauptdatei. Beispielsweise kann ein Prozess mit dem Signierer WinTcb nur DLLs mit dem Signierer Windows oder höher laden. Abbildung 3–7  Smss-Zertifikat In Windows 10 und Windows Server 2016 sind die Prozesse smss.exe, csrss.exe, services.exe und wininit.exe mit WinTcb-Lite PPL-signiert. Lsass.exe läuft in Windows für ARM-Geräte (z. B. Windows Mobile 10) als PPL und kann auf x86/x64 als PPL ausgeführt werden, wenn er durch eine Registrierungseinstellung oder eine Richtlinie entsprechend konfiguriert ist (siehe Kapitel 7). Außerdem sind einige Dienste so eingerichtet, dass sie als Windows-PPL oder als geschützter Prozess laufen, z. B. sppsvc.exe (Software Protection Platform). Auch manche Diensthostprozesse (Svchost.exe) werden mit dieser Schutzebene ausgeführt, da viele Dienste ebenfalls geschützt sind, z. B. der AppX-Bereitstellungsdienst und der Dienst des Windows-Teilsystems für Linux. Weitere Informationen über solche geschützten Dienste erhalten Sie in Kapitel 9. Für die Sicherheit des Systems ist es von entscheidender Bedeutung, dass diese Kernbinärdateien als TCB ausgeführt werden. So hat beispielsweise Csrss.exe Zugriff auf private APIs, die vom Windows-Manager (Win32k.sys) implementiert werden und über die ein Angreifer mit Administratorrechten Zugriff auf sensible Teile des Kernels gewinnen könnte. Smss.exe und 130 Kapitel 3 Prozesse und Jobs
Win­init.exe implementieren die Logik für Systemstart und -verwaltung, die unbedingt ausgeführt werden müssen, ohne dass sich ein Administrator daran zu schaffen machen kann. Windows garantiert, dass diese Binärdateien stets als WinTcb-Lite ausgeführt werden. Dadurch ist es ­beispielsweise nicht möglich, dass jemand sie startet, ohne beim Aufruf von CreateProcess die richtige Prozessschutzebene in den Prozessattributen anzugeben. Diese Garantie wird als Mindest-TCB-Liste bezeichnet und erzwingt für die Prozesse aus Tabelle 3–3, die sich in einem Systempfad befinden, eine Mindestschutz- oder -signierebene unabhängig von der Eingabe des Aufrufers. Prozess Mindestsignierebene Mindestschutzebene Smss.exe Aus der Schutzebene abgeleitet WinTcb-Lite Csrss.exe Aus der Schutzebene abgeleitet WinTcb-Lite Wininit.exe Aus der Schutzebene abgeleitet WinTcb-Lite Services.exe Aus der Schutzebene abgeleitet WinTcb-Lite Werfaultsecure.exe Aus der Schutzebene abgeleitet WinTcb-Full Sppsvc.exe Aus der Schutzebene abgeleitet Windows-Full Genvalobj.exe Aus der Schutzebene abgeleitet Windows-Full Lsass.exe SE_SIGNING_LEVEL_WINDOWS 0 Userinit.exe SE_SIGNING_LEVEL_WINDOWS 0 Winlogon.exe SE_SIGNING_LEVEL_WINDOWS 0 Autochk.exe 1 SE_SIGNING_LEVEL_WINDOWS 0 Tabelle 3–3 Mindest-TCB 1 Nur auf UEFI-Firmwaresystemen Geschützte Prozesse Kapitel 3 131
Experiment: Geschützte Prozesse in Process Explorer anzeigen In diesem Experiment sehen wir uns an, wie Process Explorer geschützte Prozesse (beider Arten) anzeigt. Starten Sie Process Explorer und aktivieren Sie auf der Registerkarte Process Image das Kontrollkästchen Protection, um die gleichnamige Spalte einzublenden: Sortieren Sie nun die Schalter Protection in absteigender Reihenfolge und scrollen Sie bis ganz nach oben. Die geschützten Prozesse werden mit ihrem Schutztyp angezeigt. Der folgende Screenshot wurde auf einem x64-Computer mit Windows 10 angefertigt: Wenn Sie einen geschützten Prozess markieren und einen Blick auf den unteren Teil werfen, sehen Sie in der DLL-Ansicht nichts. Das liegt daran, dass Process Explorer die geladenen Module mithilfe von Benutzermodus-APIs abfragt, wozu ein Zugriff erforderlich ist, der für geschützte Prozesse nicht gewährt wird. Die Ausnahme bildet der Systemprozess, der zwar geschützt ist, für den Process Explorer aber trotzdem die Liste der geladenen Kernelmodule (hauptsächlich Treiber) anzeigt, da Systemprozesse keine DLLs enthalten. Für diese Anzeige verwendet Process Explorer die System-API EnumDeviceDrivers, die kein Prozesshandle benötigt. → 132 Kapitel 3 Prozesse und Jobs
In der Handleansicht werden die gesamten Handleinformationen angezeigt. Dazu verwendet Process Explorer eine nicht dokumentierte API, die ebenfalls kein bestimmtes Prozesshandle benötigt. Das Programm kann die Prozesse identifizieren, da mit den Informationen über die Handles auch die PIDs der zugehörigen Prozesse zurückgegeben werden. Unterstützung für Drittanbieter-PPLs Der PPL-Mechanismus dehnt die Schutzmöglichkeiten für Prozesse auch auf ausführbare Dateien aus, die nicht von Microsoft entwickelt wurden. Ein Beispiel dafür bildet Anti-Malware-­ Software. Typische AM-Produkte bestehen aus den folgenden drei Hauptkomponenten: QQ Einem Kerneltreiber, der E/A-Anforderungen zum Dateisystem und zum Netzwerk abfängt und über Objekt-, Prozess- und Thread-Callbacks Blockiermöglichkeiten einrichtet QQ Einem Benutzermodusdienst (gewöhnlich unter einem privilegierten Konto), der die Treiberrichtlinien konfiguriert, Benachrichtigungen des Treibers über »interessante« Ereignisse empfängt (z. B. über eine infizierte Datei) und mit einem lokalen Server oder dem Internet kommunizieren kann QQ Einem Benutzermodus-GUI-Prozess, der Informationen an den Benutzer vermittelt und ihn ggf. Entscheidungen treffen lässt Eine Angriffsmöglichkeit für Malware besteht darin, Code in einen Prozess mit erhöhten Rechten zu injizieren – am besten noch in einen Anti-Malware-Dienst, um diesen zu modifizieren oder auszuschalten. Läuft der AM-Dienst aber als PPL, so ist weder eine Codeinjektion noch eine Beendigung des Prozesses erlaubt. Dadurch ist die AM-Software besser gegen Malware geschützt (sofern diese Exploits auf Kernelebene einsetzt). Um das zu ermöglichen, muss der zuvor beschriebene AM-Kerneltreiber über einen zugehörigen ELAM-Treiber (Early-Launch Anti Malware) verfügen. ELAM wird in Kapitel 7 ausführlicher beschrieben. Lassen Sie uns hier nur betonen, dass solche Treiber ein besonderes Anti-Malware-Zertifikat benötigen, das von Microsoft ausgestellt wird (nach eingehender Überprüfung des Softwareherstellers). Ein solcher Treiber kann in seiner ausführbaren Hauptdatei (PE) einen benutzerdefinierten Ressourcenabschnitt namens ELAMCERTIFICATEINFO enthalten, der drei zusätzliche Signierer mit jeweils drei zusätzlichen EKUs beschreibt (wobei die Signierer anhand ihres öffentlichen Schlüssels und die EKUs anhand ihrer OIDs bezeichnet werden). Wenn das Codeintegritätssystem feststellt, dass eine Datei von einem dieser drei Signierer mit einem dieser drei EKUs signiert wurde, erlaubt es dem Prozess, einen PPL von PS_PROTECTED_ANTIMALWARE_LIGHT (0x31) anzufordern. Das Standardbeispiel dafür ist Windows Defender, die eigene AM-Software von Microsoft. In Windows 10 ist ihr Dienst (MsMpEng. exe) mit dem AM-Zertifikat signiert, um ihn besser gegen Malwareangriffe gegen ihn selbst zu schützen. Das Gleiche gilt auch für den Network Inspection Server (NisSvc.exe). Geschützte Prozesse Kapitel 3 133
Minimale und Pico-Prozesse Bei den Prozessen, die wir bisher betrachtet haben, sah es so aus, als seien sie zur Ausführung von Benutzermoduscode da und würden dazu erhebliche Mengen an Datenstrukturen im Arbeitsspeicher unterbringen. Allerdings werden nicht alle Prozesse für diesen Zweck verwendet. Wie wir bereits festgestellt haben, ist der Systemprozess lediglich ein Container für die meisten Systemthreads, damit diese bei ihrer Ausführung keine anderen Benutzermodusprozesse kontaminieren, und für die Treiberhandles (Kernelhandles), damit auch diese nicht in den Besitz irgendwelcher Anwendungen geraten. Minimale Prozesse Wenn die Funktion NtCreateProcessEx vom Kernelmodus aufgerufen wird und über ein bestimmtes Flag verfügt, verhält sie sich anders als sonst und verursacht die Ausführung der API PsCreateMinimalProcess. Dadurch wird ein Prozess erstellt, dem viele der zuvor betrachteten Strukturen fehlen: QQ Es wird kein Benutzermodus-Adressraum eingerichtet, weshalb es auch den PEB und die zugehörigen Strukturen nicht gibt. QQ Dem Prozess wird keine NTDLL und auch keine Loader- oder API-Set-Information zugeordnet. QQ Es wird kein Abschnittsobjekt an den Prozess gebunden. Das heißt, mit der Ausführung oder dem Namen (der leer oder ein willkürlicher String sein kann) wird keine ausführbare Abbilddatei verknüpft. QQ In den EPROCESS-Flags wird das Flag Minimal gesetzt. Dadurch werden auch alle Threads zu Minimalthreads und es werden jegliche Benutzermodus-Zuweisungen wie der TEB (siehe Kapitel 4) oder der Benutzermodusstack vermieden. Wie Sie in Kapitel 2 gesehen haben, gibt es in Windows 10 mindestens zwei minimale Prozesse, nämlich den System- und den Speicherkomprimierungsprozess. Ist die virtualisierungsgestützte Sicherheit aktiviert, kann mit dem Secure-System-Prozess noch ein dritter hinzukommen (siehe Kapitel 2 und 7). Die zweite Möglichkeit, um minimale Prozesse auf einem Windows 10-System ausführen zu lassen, besteht darin, das optionale Windows-Teilsystem für Linux (WSL) zu aktivieren (siehe Kapitel 2). Dadurch wird ein im Lieferumfang von Windows enthaltener Pico-Provider installiert, der aus den Treibern Lxss.sys und LxCore.sys besteht. Pico-Prozesse Minimale Prozesse sind nur von beschränktem Nutzen, um Kernelkomponenten Zugriff auf den virtuellen Benutzermodus-Adressraum zu geben und diesen zu schützen. Pico-Prozesse dagegen spielen eine wichtigere Rolle, da sie einer besonderen Komponente – dem Pico-Anbieter – ermöglichen, die meisten Aspekte ihrer Ausführung zu steuern. Damit kann ein solcher Anbieter letzten Endes das Verhalten eines völlig anderen Betriebssystemkernels emulieren, ohne dass die 134 Kapitel 3 Prozesse und Jobs
zugrunde liegende Benutzermodus-Binärdatei weiß, dass sie auf einem Windows-Betriebssystem läuft. Im Grunde genommen handelt es sich dabei um eine Implementierung des Microsoft Research-Projekts Drawbridge, das auf ähnliche Weise auch SQL Server für Linux unterstützt (wenn auch mit einem Windows-Bibliotheksbetriebssystem oberhalb des Linux-Kernels). Um auf einem System Pico-Prozesse verwenden zu können, muss zunächst ein Anbieter vorhanden sein. Er kann mit der API PsRegisterPicoProvider registriert werden, wobei jedoch eine besondere Regel gilt: Der Pico-Anbieter muss vor allen anderen Drittanbietertreibern geladen werden (einschließlich der Boottreiber). Nur jeweils ein einziger aus einem eingeschränkten Satz von etwa einem Dutzend Kerntreibern darf diese API aufrufen, bevor diese Funktionalität deaktiviert wird. Diese Kerntreiber müssen mit einem Microsoft-Signiererzertifikat und einer Windows-Komponenten-EKU signiert sein. Auf Windows-Systemen, auf denen die optionale WSL-Komponente aktiviert ist, handelt es sich hierbei um den Treiber Lxss.sys, der als Stubtreiber fungiert, bis etwas später der Treiber LxCore.sys geladen wird und die Verantwortung für den Pico-Anbieter übernimmt, indem er die Dispatchtabellen auf sich überträgt. Zurzeit kann sich auch nur ein einziger Kerntreiber als Pico-Anbieter registrieren. Wenn ein Pico-Anbieter die Registrierungs-API aufruft, erhält er eine Reihe von Funktionszeigern, mit denen er Pico-Prozesse erstellen und verwalten kann: QQ Zwei Funktionen, um Pico-Prozesse bzw. Pico-Threads zu erstellen QQ Eine Funktion, um den Kontext (einen willkürlichen Zeiger, mit dem der Anbieter bestimmte Daten speichern kann) eines Pico-Prozesses abzurufen, eine, um ihn festzulegen, und zwei weitere Funktionen, um das Gleiche für Pico-Threads zu tun. Damit wird das Feld PicoContext in der ETHREAD- bzw. EPROCESS-Struktur ausgefüllt. QQ Eine Funktion, um die CPU-Kontextstruktur (CONTEXT) eines Pico-Threads abzurufen, und eine, um ihn festzulegen QQ Eine Funktion, um das FS- und GS-Segment eines Pico-Threads zu ändern. Diese Segmente werden normalerweise von Benutzermoduscode verwendet, um auf eine lokale Threadstruktur zu verweisen (wie den TEB in Windows). QQ Zwei Funktionen, um Pico-Threads bzw. Pico-Prozesse zu beenden QQ Eine Funktion, um einen Pico-Thread anzuhalten, und eine weitere, um ihn wieder aufzunehmen Mithilfe dieser Funktionen kann der Pico-Anbieter maßgeschneiderte Prozesse und Threads erstellen, wobei deren ursprünglicher Startzustand, die Segmentregister und die zugehörigen Daten seiner Kontrolle unterliegen. Das allein macht es aber noch nicht möglich, ein anderes Betriebssystem zu emulieren. Es gibt noch einen zweiten Satz von Funktionszeigern, die jedoch vom Anbieter zum Kernel übertragen werden und als Callbacks fungieren, wenn ein Pico-Thread oder -Prozess bestimmte Aktivitäten ausführt. QQ Ein Callback für den Fall, dass ein Pico-Thread mithilfe der Anweisung SYSCALL einen Systemaufruf vornimmt QQ Ein Callback für den Fall, dass ein Pico-Thread eine Ausnahme auslöst QQ Ein Callback für den Fall, dass in einem Pico-Thread ein Fehler bei einer Sondierungs- und Sperroperation an einer Speicherdeskriptorliste (MDL) auftritt Minimale und Pico-Prozesse Kapitel 3 135
QQ Ein Callback für den Fall, dass ein Aufrufer den Namen eines Pico-Prozesses anfordert QQ Ein Callback für den Fall, dass die Ereignisverfolgung für Windows (ETW) die Benutzermodus-Stackablaufverfolgung eines Pico-Prozesses anfordert QQ Ein Callback für den Fall, dass eine Anwendung versucht, ein Handle für einen Pico-Prozess oder -Thread zu öffnen QQ Ein Callback für den Fall, dass jemand die Beendigung eines Pico-Prozesses anfordert QQ Ein Callback für den Fall, dass ein Pico-Prozess oder -Thread unerwartet beendet wird Der Pico-Anbieter nutzt auch den in Kapitel 7 beschriebenen Kernelpatchschutz (Kernel Patch Protection, KPP), um seine Callbacks und Systemaufrufe zu schützen und um zu verhindern, dass sich gefälschte oder schädliche Pico-Anbieter oberhalb von ihm registrieren. Angesichts dieses beispiellosen Zugriffs auf alle möglichen Benutzer-Kernel-Übergänge und sichtbaren Kernel-Benutzer-Interaktionen zwischen sich selbst und der Welt können Pico-Prozesse und -Threads offensichtlich komplett von einem Pico-Anbieter (und entsprechenden Benutzermodusbibliotheken) gekapselt werden, um als Wrapper für eine ganz andere Kernelimplementierung als die von Windows zu dienen. (Wobei es natürlich Ausnahmen gibt, da nach wie vor die Regeln für Threadplanung und Speicherverwaltung gelten, z. B. für Commits.) Ordnungsgemäß geschriebene Anwendungen sollten nicht von solchen internen Algorithmen beeinflusst werden, da diese sich sogar in dem Betriebssystem ändern können, in dem die Anwendungen normalerweise laufen. Pico-Anbieter sind daher letzten Endes maßgeschneiderte Kernelmodule, die die notwendigen Callbacks für die Reaktion auf die von einem Pico-Prozess hervorgerufenen Ereignisse (siehe weiter vorn) implementieren. Dadurch kann das WSL unveränderte Linux-ELF-Binärdateien im Benutzermodus ausführen. Eingeschränkt wird diese Möglichkeit durch die Vollständigkeit der Systemaufrufemulation und des damit verbundenen Funktionsumfangs. Zur Abrundung des Themas sehen Sie in Abb. 3–8 die Strukturen von NT-, minimalen und Pico-Prozessen. NT-Prozess Minimaler Prozess Pico-Prozess Prozessumgebungs­ block (PEB) Threadumgebungs­ block (TEB) Gemeinsame Benutzerdaten NTDLL.DLL System­ aufrufe System­ aufrufe System­ aufrufe Benutzermodus Exekutive und Kernel Pico-Anbieter (Lxss/LXCore) Kernelmodus Abbildung 3–8 Arten von Prozessen 136 Kapitel 3 Prozesse und Jobs
Trustlets (sichere Prozesse) Wie bereits in Kapitel 2 erwähnt, enthält Windows neue VBS-Merkmale (Virtualization-Based Security) wie DeviceGuard und CredentialGuard, die mithilfe eines Hypervisors die Sicherheit des Betriebssystems und der Benutzerdaten verstärken. Eines dieser Merkmale, nämlich CredentialGuard (ausführlich beschrieben in Kapitel 7), läuft in der neuen Umgebung des isolierten Benutzermodus, der zwar immer noch unprivilegiert ist (Ring 3), aber eine virtuelle Vertrauens­ ebene von 1 (VTL 1) aufweist. Dadurch ist er von der regulären Ebene VTL 0 geschützt, auf der sich sowohl der NT-Kernel (Ring 0) als auch die Anwendungen (Ring 3) befinden. Sehen wir uns nun an, wie der Kernel solche Prozesse zur Ausführung einrichtet und welche Datenstrukturen diese Prozesse verwenden. Der Aufbau von Trustlets Trustlets sind zwar reguläre PE-Dateien (Windows Portable Executables), weisen aber einige IUM-spezifische Eigenschaften auf: QQ Aufgrund der eingeschränkten Anzahl von Systemaufrufen, die für Trustlets zur Verfügung stehen, können sie nur Funktionen aus einer eingeschränkten Menge von Windows-­ System-DLLs importieren (C/C++ Runtime, KernelBase, Advapi, RPC Runtime, CNG Base Crypto und NTDLL). Mathematische DLLs, die nur Datenstrukturen bearbeiten (wie NTLM, ASN.1 usw.), können jedoch ebenfalls verwendet werden, da sie keine Systemaufrufe durchführen. QQ Sie können Funktionen von einer IUM-spezifischen System-DLL namens Iumbase importieren, die ihnen zur Verfügung gestellt wird. Diese DLL enthält die grundlegende IUMSystem-API mit Unterstützung für Mailslots, Speicherboxen, Kryptografie usw. Diese Bibliothek führt letzten Endes Aufrufe in Iumdll.dll durch, der VTL-1-Version von Ntdll.dll, und enthält sichere Systemaufrufe (also solche, die vom sicheren Kernel implementiert werden und nicht an den normalen VTL-0-Kernel übergeben werden). QQ Sie enthalten den PE-Abschnitt .tPolicy mit der exportierten globalen Variablen s_Ium​ PolicyMetadata. Damit werden Metadaten für den sicheren Kernel bereitgestellt, um Richtlinieneinstellungen für den VTL-0-Zugriff des Trustlets zu implementieren (z. B. um Debugging zu erlauben, Unterstützung für Absturzabbilder bereitzustellen usw.). QQ Sie werden mit einem Zertifikat signiert, dass die IUM-EKU enthält (1.3.6.1.4.311.10.3.37). Abb. 3–9 zeigt die Zertifikatdaten für LsaIso.exe mit dieser EKU. Außerdem muss beim Start von Trustlets mit CreateProcess ein besonderes Prozessattribut verwendet werden, um die Ausführung im IUM anzufordern und um besondere Starteigenschaften anzugeben. Die Richtlinien-Metadaten und die Prozessattribute beschreiben wir in den folgenden Abschnitten. Trustlets (sichere Prozesse) Kapitel 3 137
Abbildung 3–9 Trustlet-EKU im Zertifikat Richtlinien-Metadaten für Trustlets Zu den Richtlinien-Metadaten gehören verschiedene Optionen, um festzulegen, wie »zugänglich« das Trustlet von VLT 0 aus sein soll. Sie werden durch eine Struktur beschrieben, die sich in der zuvor erwähnten exportierten Variablen s_IumPolicyMetadaten befindet, und enthalten die Versionsnummer (zurzeit 1) und die Trustlet-ID, also eine eindeutige Zahl zur Bezeichnung eines bestimmten der bekannten Trustlets. Beispielsweise hat BioIso.exe die Trustlet-ID 4. Des Weiteren gehört zu den Metadaten ein Array mit Richtlinienoptionen. Zurzeit werden die Optionen aus Tabelle 3–4 unterstützt. Da diese Richtlinien Teil der signierten ausführbaren Daten sind, verletzt jeder Versuch, sie zu ändern, die IUM-Signatur und verhindert damit eine Ausführung. Richtlinie Bedeutung ETW Aktiviert oder deaktiviert ETW Debug Konfiguriert das Debugging Crash Dump Aktiviert oder deaktiviert Absturzabbilder Crash Dump Key Gibt den öffentlichen Schlüssel zur Verschlüsselung von Absturzabbildern an Weitere Informationen Das Debugging kann jederzeit aktiviert sein; nur bei deaktiviertem SecureBoot; oder bei Bedarf über einen Challenge/ Response-Mechanismus. Absturzabbilder können an das Microsoft-Produktteam übermittelt werden, das über den privaten Schlüssel für die Entschlüsselung verfügt. → 138 Kapitel 3 Prozesse und Jobs
Richtlinie Bedeutung Weitere Informationen Crash Dump GUID Gibt den eindeutigen Bezeichner für den Absturzabbildschlüssel an Dadurch kann das Produktteam mehrere Schlüssel verwenden bzw. erkennen. Parent Security Descriptor SDDL-Format Dient zur Validierung, dass der Besitzeroder Elternprozess der erwartete ist. Parent Security Descriptor Revision Revisions-ID des SDDL-Formats Dient zur Validierung, dass der Besitzeroder Elternprozess der erwartete ist. SVN Sicherheitsversion Eine eindeutige Zahl, die das Trustlet (zusammen mit seiner ID) bei der Verschlüsselung von AES256/GCM-Nach­ richten nutzen kann. Device ID PCI-Bezeichner des sicheren Geräts Das Trustlet kann nur mit einem sicheren Gerät kommunizieren, das den entsprechenden PCI-Bezeichner aufweist. Capability Aktiviert VTL-1-Fähigkeiten Aktiviert den Zugriff auf die API Create Secure Section; DRM- und Benutzermodus-MMIO-Zugriff auf sichere Geräte; und APIs für die sichere Speicherung. Scenario ID Gibt die Szenario-ID für die Binärdatei an Trustlets müssen diesen GUID angeben, wenn sie sichere Abbildabschnitte erstellen, um sicherzustellen, dass es sich um ein bekanntes Szenario handelt. Tabelle 3–4 Trustlet-Richtlinienoptionen Attribute von Trustlets Der Start eines Trustlets erfordert die ordnungsgemäße Verwendung des Attributs PS_CP_­ SECURE_PROCESS. Es dient zur Bestätigung, dass der Aufrufer wirklich ein Trustlet erstellen möchte, und zur Überprüfung, dass das Trustlet, das der Aufrufer auszuführen glaubt, auch tatsächlich das Trustlet ist, das ausgeführt wird. Dazu wird eine Trustlet-ID in das Attribut eingebettet. Sie muss mit der Trustlet-ID in den Richtlinien-Metadaten übereinstimmen. Es können auch noch weitere Attribute angegeben werden, die in Tabelle 3–5 aufgeführt sind. Attribut Bedeutung Weitere Informationen Mailbox Key Dient zum Abrufen von Postfachdaten Über Postfächer kann ein Trustlet Daten mit VLT-0-Anwendungen gemeinsam nutzen, sofern der Trustlet-Schlüssel bekannt ist. Collaboration ID Legt die Kollaborations-ID fest, die zur Verwendung der IUMAPI Secure Storage benötigt wird Die Secure-Storage-API ermöglicht Trustlets mit gemeinsamer Kollaborations-ID, Daten untereinander gemeinsam zu nutzen. Ist keine Kollaborations-ID vorhanden, wird stattdessen die ID der Trustlet-Instanz verwendet. TK Session ID Die Sitzungs-ID, die bei kryptografischen Vorgängen verwendet wird Tabelle 3–5 Trustlet-Attribute Trustlets (sichere Prozesse) Kapitel 3 139
Integrierte System-Trustlets Zurzeit enthält Windows 10 die fünf Trustlets, die in Tabelle 3–6 aufgeführt sind. Die Trustlet-ID 0 steht für den sicheren Kernel selbst. Binärdatei mit Trustlet-ID in Klammern Bezeichnung Richtlinienoptionen LsaIso.exe (1) Credential and Key Guard Trustlet Allow ETW, Disable Debugging, Allow Encrypted Crash Dump Vmsp.exe (2) Secure Virtual Machine Worker (vTPM Trustlet) Allow ETW, Disable Debugging, Dis­ able Crash Dump, Enable Secure Storage Capability, Verify Parent Security Descriptor is S-1-5-83-0 (NT VIRTUAL MACHINE\Virtual Machines) Unbekannt (3) vTPM Key Enrollment Trustlet Unbekannt BioIso.exe (4) Secure Biometrics Trustlet Allow ETW, Disable Debugging, Allow Encrypted Crash Dump FsIso.exe (5) Secure Frame Server Trustlet Disable ETW, Allow Debugging, Enable Create Secure Section Capability, Use Scenario ID { AE53FC6E-8D89-44889D2E-4D008731C5FD} Tabelle 3–6 Integrierte Trustlets Trustlet-Identität Trustlets verfügen über verschiedene Identitätsangaben: QQ Trustlet-ID Dies ist ein Integerwert, der in den Richtlinien-Metadaten des Trustlets hartcodiert ist und in den Prozesserstellungsattributen für das Trustlet verwendet werden muss. Dadurch weiß das System, dass es nur eine Handvoll Trustlets gibt. Anhand der ID wird auch sichergestellt, dass Aufrufer tatsächlich das gewünschte Trustlet starten. QQ Trustlet-Instanz Hierbei handelt es sich um eine kryptografisch sichere 16-Byte-Zufallszahl, die vom sicheren Kernel erstellt wird. Gibt es keine Kollaborations-ID, so wird die Trustlet-Instanz herangezogen, um dafür zu sorgen, dass Secure-Storage-APIs nur dieser einen Instanz des Trustlets erlauben, Daten in ihr Storage-BLOB zu stellen und von dort abzurufen. QQ Kollaborations-ID Diese ID wird verwendet, wenn ein Trustlet anderen Trustlets mit derselben ID oder anderen Instanzen desselben Trustlets den gemeinsamen Zugriff auf ein Secure-Storage-BLOB erlauben möchte. Ist diese ID vorhanden, wird die Instanz-ID beim Aufruf der Get- und Put-APIs ignoriert. QQ Sicherheitsversion (SVN) Sie wird für Trustlets verwendet, die einen starken kryptografischen Beweis für den Ursprung signierter oder verschlüsselter Daten verlangen, etwa bei der Verschlüsselung von AES256/GCM-Daten durch Credential- und KeyGuard und für den Dienst Cryptograph Report. 140 Kapitel 3 Prozesse und Jobs
QQ Szenario-ID Sie wird für Trustlets verwendet, die benannte sichere Kernelobjekte erstellen, z. B. sichere Abschnitte. Diese Objekte werden im Namensraum mit diesem GUID gekennzeichnet, um zu bestätigen, dass das Trustlet sie im Rahmen eines vorherbestimmten Szenarios erstellt hat. Andere Trustlets, die diese benannten Objekte öffnen wollen, müssen über dieselbe Szenario-ID verfügen. Es ist zwar möglich, dass mehr als eine Szenario-ID vorhanden ist, doch zurzeit verwenden Trustlets nicht mehr als eine. IUM-Dienste Die Ausführung als Trustlet bietet nicht nur den Vorteil des Schutzes gegen Angriffe aus der normalen VLT-0-Umgebung, sondern bietet auch Zugriff auf privilegierte und geschützte sichere Systemaufrufe, die der sichere Kernel nur für Trustlets bereitstellt. Dazu gehören die folgenden Dienste: QQ Sichere Geräte (IumCreateSecureDevice, IumDmaMapMemory, IumGetDmaEnabler, IumMapSecureIo, IumProtectSecureIo, IumQuerySecureDeviceInformation, IopUnmapSecureIo, IumUpdateSecureDeviceState) Sie bieten Zugriff auf sichere ACPI- und PCI-Geräte, die nicht von VTL 0 aus zugänglich sind und exklusiv dem sicheren Kernel (und seinen Hilfsdiensten für die sichere HAL und das sichere PCI) gehören. Trustlets mit entsprechenden Fähigkeiten (siehe den Abschnitt »Richtlinien-Metadaten für Trustlets« weiter vorn in diesem Kapitel) können die Register solcher Geräte auf den VTL-1-IUM abbilden und auch Übertragungen mit direktem Speicherzugriff vornehmen (Direct Memory Access, DMA). Außerdem können sie als Benutzermodus-Gerätetreiber für solche Hardware dienen, indem sie das Framework für sichere Geräte (Secure Device Framework, SDF) aus SDFHost.dll verwenden. Diese Funktionalität wird für biometrische Verfahren in Windows Hello verwendet, z. B. für die Verwendung von USB-Smartcards (über PCI) und von Webcams und Fingerabdrucksensoren (über ACPI). QQ Sichere Abschnitte (IumCreateSecureSection, IumFlushSecureSectionBuffers, IumGet­ ExposedSecureSection, IumOpenSecureSection) Über sichere Abschnitte ist es möglich, physische Seiten gemeinsam mit einem VTL-0-Treiber zu nutzen (der dazu VslCreateSecureSection verwenden muss). Es können auch Daten als benannte sichere Abschnitte innerhalb von VTL 1 mit anderen Trustlets oder anderen Instanzen desselben Trustlets geteilt werden (wobei der zuvor im Abschnitt »Trustlet-Identität« erwähnte Identitätsmechanismus eingesetzt wird). Um dies tun zu können, müssen die Trustlets über die im Abschnitt »Richt­linienMetadaten für Trustlets« erwähnte Fähigkeit Secure Sections verfügen. QQ Postfächer (IumPostMailbox) Damit kann sich ein Trustlet bis zu acht Slots von maximal ca. 4 KB Daten mit einer Komponente im normalen VLT-0-Kernel teilen, der dazu VslRetrieveMailbox aufruft und die Slot-ID und den geheimen Postfachschlüssel angibt. Dies wird unter anderem von Vid.sys in VTL 0 verwendet, um verschiedene Geheimnisse abzurufen, die vTPM im Trustlet Vmsp.exe nutzt. Trustlets (sichere Prozesse) Kapitel 3 141
QQ Identitätsschlüssel (IumGetIdk) Damit kann das Trustlet einen eindeutig kennzeichnenden Entschlüsselungsschlüssel oder einen Signierschlüssel abrufen. Das Schlüssel­ material bezeichnet eindeutig den Computer und kann nur von einem Trustlet bezogen werden. Dies ist ein entscheidender Aspekt von CredentialGuard, um den Computer zu authentifizieren und zu bestätigen, dass die Anmeldeinformationen tatsächlich aus dem IUM kommen. QQ Kryptografiedienste (IumCrypto) Sie erlauben einem Trustlet, Daten mit einem lokalen oder Bootsitzungsschlüssel zu verschlüsseln und zu entschlüsseln. Dieser Schlüssel wird vom sicheren Kernel erstellt und steht nur im IUM zur Verfügung. Des Weiteren kann ein Trustlet damit ein TPM-Bindungshandle erlangen; den FIPS-Modus des sicheren Kernels abrufen; einen Seed des Zufallszahlengenerators gewinnen, der ausschließlich vom sicheren Kernel für IUM generiert wird; einen Bericht mit IDK-Signatur, SHA-2-Hash, Zeitstempel und der Identität und SVN des Trustlets anlegen; eine Abbilddatei seiner Richtlinien-Metadaten unabhängig davon erstellen, ob sie jemals mit einem Debugger verknüpft wurden oder nicht; und jegliche andere angeforderten Daten generieren, die seiner Kontrolle unterliegen. Das lässt sich als eine Art von TPM-Maßnahme nutzen, mit der das Trustlet beweisen kann, dass es nicht manipuliert worden ist. QQ Sicherer Speicher (IumSecureStorageGet, IumSecureStoragePut) Dadurch bekommt das Trustlet die Fähigkeit Secure Storage (wie zuvor im Abschnitt »Richtlinien-Metadaten für Trustlets« beschrieben), um BLOBs willkürlicher Größe zu speichern und abzurufen. Der Zugriff auf das BLOB kann auf die jeweilige Trustlet-Instanz beschränkt sein oder anderen Trustlets mit derselben Kollaborations-ID offenstehen. Für Trustlets zugängliche Systemaufrufe In dem Bemühen, seine Angriffsfläche zu reduzieren, stellt der sichere Kernel nur eine Teilmenge von weniger als fünfzig der Hunderte von Systemaufrufen zur Verfügung, die eine normale VLT-0-Anwendung nutzen kann. Diese Aufrufe bilden das absolute Minimum für eine Kompatibilität mit den DLLs, die von Trustlets verwendet werden können (welche das sind, erfahren Sie im Abschnitt »Der Aufbau von Trustlets«), und den Diensten, die zur Unterstützung der RPC-Laufzeit (Rpcrt4.dll) und der ETW-Verfolgung erforderlich sind. QQ Worker-Factory- und Thread-APIs Sie unterstützen die von RPCs verwendete Threadpool-API und die vom Lader verwendeten TLS-Slots. QQ Prozessinformations-API Sie unterstützt TLS-Slots und die Threadstackzuweisung. QQ Ereignis-, Semaphoren-, Warte- und Vervollständigungs-APIs Threadpools und Synchronisierung. Sie unterstützten QQ ALPC-APIs (Advanced Local Procedure Calls) Sie unterstützen lokale RPCs über den Transport ncalrpc. QQ Systeminformations-API Sie ermöglicht das Lesen von Informationen über den sicheren Start, grundlegenden und NUMA-Systeminformationen für Kernel32.dll und die Threadpoolskalierung, Leistungsinformationen und einigen Zeitinformationen. QQ Token-API Sie bietet minimale Unterstützung für RPC-Identitätswechsel. 142 Kapitel 3 Prozesse und Jobs
QQ APIs für virtuelle Speicherzuweisung Manager des Benutzermodus. Sie unterstützen Zuweisungen durch den Heap-­ QQ Abschnitts-APIs Sie unterstützen den Lader (für DLL-Abbilder) und die Funktionalität sicherer Abschnitte (sobald sie durch die zuvor angegebenen sicheren Systemaufrufe erstellt bzw. verfügbar gemacht worden sind). QQ Steuer-API für die Ablaufverfolgung Sie unterstützt ETW. QQ Ausnahme- und Fortsetzungs-API   Sie unterstützt die strukturierte Ereignisbehandlung (Structured Exception Handling, SEH). Diese Liste zeigt deutlich, dass es keine Unterstützung für Operationen wie die folgenden gibt: Geräte-E/A, unabhängig davon, ob es sich um Dateien oder physische Geräte handelt (es gibt schon einmal keine CreateFile-API); Registrierungs-E/A; Erstellen anderer Prozesse; jegliche Verwendung von Grafik-APIs (es gibt keinen Win32k.sys-Treiber in VTL 1). Trustlets sind als isolierte Arbeits-Back-Ends in VTL 1 für ihre komplexen Front-Ends in VTL 0 gedacht. Als einzige Kommunikationsmechanismen stehen ihnen ALPC und bereitgestellte sichere Abschnitte zur Verfügung (wobei ihnen die Handles dieser sicheren Abschnitte über ALPC vermittelt werden müssen). In Kapitel 7 schauen wir uns die Implementierung des Trustlets LsaIso.exe genauer an, das für CredentialGuard und KeyGuard zuständig ist. Experiment: Sichere Prozesse identifizieren Abgesehen von ihrem Namen können Sie sichere Prozesse im Kerneldebugger auf zwei verschiedene Weisen erkennen. Erstens verfügt jeder sichere Prozess über eine sichere PID, die in der Handletabelle des sicheren Kernels für sein Handle steht. Sie wird vom normalen VLT-0-Kernel verwendet, wenn er Threads in dem Prozess erstellt oder dessen Beendigung anfordert. Zweitens ist mit den Threads ein Threadcookie verknüpft, das ihren Index in der Threadtabelle des sicheren Kernels angibt. Probieren Sie Folgendes im Kerneldebugger aus: lkd> !for_each_process .if @@(((nt!_EPROCESS*)${@#Process})->Pcb.SecurePid) { .printf "Trustlet: %ma (%p)\n", @@(((nt!_EPROCESS*)${@#Process}) ->ImageFileName), @#Process } Trustlet: Secure System (ffff9b09d8c79080) Trustlet: LsaIso.exe (ffff9b09e2ba9640) Trustlet: BioIso.exe (ffff9b09e61c4640) lkd> dt nt!_EPROCESS ffff9b09d8c79080 Pcb.SecurePid +0x000 Pcb : +0x2d0 SecurePid : 0x00000001'40000004 lkd> dt nt!_EPROCESS ffff9b09e2ba9640 Pcb.SecurePid +0x000 Pcb : +0x2d0 SecurePid : 0x00000001'40000030 → Trustlets (sichere Prozesse) Kapitel 3 143
lkd> dt nt!_EPROCESS ffff9b09e61c4640 Pcb.SecurePid +0x000 Pcb : +0x2d0 SecurePid : 0x00000001'40000080 lkd> !process ffff9b09e2ba9640 4 PROCESS ffff9b09e2ba9640 SessionId: 0 Cid: 0388 Peb: 6cdc62b000 ParentCid: 0328 DirBase: 2f254000 ObjectTable: ffffc607b59b1040 HandleCount: 44. Image: LsaIso.exe THREAD ffff9b09e2ba2080 Cid 0388.038c Teb: 0000006cdc62c000 Win32Thread: 0000000000000000 WAIT lkd> dt nt!_ETHREAD ffff9b09e2ba2080 Tcb.SecureThreadCookie +0x000 Tcb +0x31c SecureThreadCookie : 9 Der Ablauf von CreateProcess Wir haben uns bereits die verschiedenen Datenstrukturen für die Bearbeitung und Verwaltung des Prozesszustands und die Werkzeuge und Debuggerbefehle angesehen, mit denen sie sich untersuchen lassen. In diesem Abschnitt beschäftigen wir uns damit, wie diese Datenstrukturen erstellt und ausgefüllt und wie Prozesse erstellt und beendet werden. Wie bereits erwähnt, rufen alle Funktionen zur Prozesserstellung letzten Endes CreateProcessInternalW auf, weshalb wir damit beginnen. Um einen Windows-Prozess zu erstellen, werden mehrere Schritte in drei Teilen des Betriebssystems ausgeführt: in der clientseitigen Windows-Bibliothek Kernel32.dll (die eigentliche Arbeit beginnt mit CreateProcessInternalW), in der Windows-Exekutive und im Windows-Teilsystemprozess (Csrss). Aufgrund der Teilsystemarchitektur für mehrere Umgebungen wird die Erzeugung eines Exekutivprozessobjekts (das auch andere Teilsysteme nutzen können) von den Arbeiten zum Erstellen eines Windows-Teilsystemprozesses getrennt. Die folgende Beschreibung des Ablaufs der Windows-Funktion CreateProcess ist zwar kompliziert, doch denken Sie daran, dass dieser Teil der Arbeit nur für die zusätzliche Semantik des Windows-Teilsystems erforderlich ist und nicht zu der grundlegenden Arbeit zum Erstellen eines Exekutivprozessobjekts gehört. Die folgende Übersicht zeigt die Hauptphasen beim Erstellen eines Prozesses mit den CreateProcess*-Funktionen von Windows. Die in den jeweiligen Phasen ausgeführten Operationen werden in den anschließenden Abschnitten ausführlich erläutert. Viele Schritte von CreateProcess hängen mit der Einrichtung des virtuellen Adress­ raums zusammen, weshalb dabei viele Begriffe und Strukturen der Speicherverwaltung erwähnt werden, deren Definition Sie in Kapitel 5 finden. 144 Kapitel 3 Prozesse und Jobs
1. Parameter validieren; Windows-Teilsystemflags und -optionen in ihre nativen Gegenstücke umwandeln; Attributliste analysieren, validieren und in ihr natives Gegenstück umwandeln. 2. Die Abbilddatei (.exe) öffnen, die in dem Prozess ausgeführt werden soll. 3. Das Windows-Exekutivprozessobjekt erstellen. 4. Den ursprünglichen Thread erstellen (Stack, Kontext und Windows-Exekutivthreadobjekt). 5. Spezifische Prozessinitialisierung für das Windows-Teilsystem durchführen. 6. Ausführung des ursprünglichen Threads starten (sofern nicht das Flag CREATE_SUSPENDED angegeben war). 7. Initialisierung des Adressraums im Kontext des neuen Prozesses und Threads abschließen (z. B. durch Laden der erforderlichen DLLs) und die Ausführung des Programmeintrittspunkts beginnen. Abb. 3–10 gibt einen Überblick über die Phasen, die Windows beim Erstellen eines Prozesses durchläuft. Prozess erstellen Phase 1 Parameter und Flags konvertieren und validieren Phase 2 EXE öffnen und Abschnittsobjekt erstellen Phasen 3+4 Prozess- und ThreadObjekte erstellen Phase 5 Spezifische Prozess­ini­tia­ lisierung für das WindowsTeilsystem durchführen Phase 6 Ausführung des ur­sprüng­ lichen Threads starten Windows-Teilsystem Für neuen Prozess und Thread einrichten Neuer Prozess Phase 6 Abschließende Prozessinitialisierung Ausführung des Eintrittspunkts beginnen Fertig Abbildung 3–10 Die Hauptphasen der Prozesserstellung Phase 1: Parameter und Flags konvertieren und validieren Bevor die Funktion CreateProcessInternalW das auszuführende Image öffnet, führt sie folgende Schritte aus: 1. Die Prioritätsklasse für den neuen Prozess wird in Form unabhängiger Bits im Parameter CreationFlags der CreateProcess*-Funktionen angegeben. Daher ist es möglich, bei einem CreateProcess*-Aufruf mehrere Prioritätsklassen anzugeben. Windows weist dem Prozess die Klasse mit der niedrigsten Priorität zu. Der Ablauf von CreateProcess Kapitel 3 145
Es sind sechs Prioritätsklassen definiert, denen jeweils eine Zahl zugeordnet ist: • Leerlauf oder niedrig (4) • Niedriger als normal (6) • Normal (8) • Höher als normal (10) • Hoch (13) • Echtzeit (24) Die Prioritätsklasse wird als Basispriorität für die in dem Prozess erstellten Threads verwendet. Dieser Wert wirkt sich nicht direkt auf den Thread aus, sondern nur auf die enthaltenen Threads. Eine Erläuterung der Prioritätsklassen und ihrer Auswirkung auf die Threadplanung erhalten Sie in Kapitel 4. 2. Wurde für den neuen Prozess keine Prioritätsklasse angegeben, wird Normal verwendet. Wurde die Prioritätsklasse Echtzeit angegeben und hat der Aufrufer des Prozesses nicht das Recht Anheben der Zeitplanungspriorität (SE_INC_BASE_PRIORITY), dann wird stattdessen die Klasse Hoch verwendet. Die Prozesserstellung schlägt also nicht fehl, nur weil der Aufrufer unzureichende Berechtigungen hat, um einen Prozess mit Echtzeitpriorität anzulegen. Stattdessen erhält der neue Prozess einfach eine niedrigere Priorität. 3. Verlangen die Erstellungsflags ein Debugging des Prozesses, so stellt Kernel32 eine Verbindung zum nativen Debuggingcode in Ntdll.dll her, indem er DbgUiConnectToDbg aufruft und ein Handle für das Debugobjekt vom aktuellen Threadumgebungsblock (TEB) abruft. 4. Kernel32.dll richtet den standardmäßigen harten Fehlermodus ein, wenn dies in den Erstellungsflags angegeben ist. 5. Die vom Benutzer definierte Attributliste wird vom Format des Windows-Teilsystems in das native Format umgewandelt und um interne Attribute ergänzt. Die möglichen zusätzlichen Attribute finden Sie in Tabelle 3–7 zusammen mit ihren dokumentierten Windows-API-Gegenstücken, falls vorhanden. Die an CreateProcess*-Aufrufe übergebene Attributliste ermöglicht es, an den Aufrufer Informationen zurückzugeben, die über einen einfachen Statuscode hinausgehen, z. B. die TEB-Adresse des ursprünglichen Threads oder Informationen über den Abbildabschnitt. Das ist für geschützte Prozesse notwendig, da der Elternprozess diese Information nach dem Erstellen des Kindprozesses nicht abfragen kann. Natives Attribut Entsprechendes W32Attribut Typ Beschreibung PS_CP_PARENT_PROCESS PROC_THREAD_ATTRIBUTE_ PARENT_PROCESS. Auch bei Rechteerhöhung verwendet. Eingabe Handle für den Elternprozess PS_CP_DEBUG_OBJECT Nicht verfügbar. Wird bei der Verwendung von DEBUG_ PROCESS als Flag genutzt. Eingabe Debugobjekt, wenn der Prozess im Debuggingmodus gestartet wird 146 Kapitel 3 Prozesse und Jobs →
Natives Attribut Entsprechendes W32Attribut Typ Beschreibung PS_CP_PRIMARY_TOKEN Nicht verfügbar. Wird bei der Verwendung von CreateProcessAsUser/ WithTokenW genutzt. Eingabe Prozesstoken, wenn CreateProcessAsUser verwendet wurde PS_CP_CLIENT_ID Nicht verfügbar. Wird von der Win32-API als Parameter zurückgegeben (PROCESS_ INFORMATION). Ausgabe Gibt die TID und PID des ursprünglichen Threads bzw. Prozesses zurück. PS_CP_TEB_ADDRESS Nicht verfügbar. Intern verwendet und nicht verfügbar gemacht. Ausgabe Gibt die Adresse des TEB für den ursprünglichen Thread zurück. PS_CP_FILENAME Nicht verfügbar. Wird als Parameter in CreateProcess-­ APIs verwendet. Eingabe Der Name des zu erstellenden Prozesses PS_CP_IMAGE_INFO Nicht verfügbar. Intern verwendet und nicht verfügbar gemacht. Ausgabe Gibt SECTION_IMAGE_­ INFORMATION mit Informationen über die Version, die Flags und das Teilsystem der ausführbaren Datei sowie die Stackgröße und den Eintrittspunkt zurück. PS_CP_MEM_RESERVE Nicht verfügbar. Intern von SMSS und CSRSS verwendet. Eingabe Ein Array von Reservierungen für virtuellen Speicher, die beim ursprünglichen Erstellen des Prozessadressraums durchgeführt werden sollen. Die Verfügbarkeit wird garantiert, da noch keine anderen Zuweisungen stattgefunden haben. PS_CP_PRIORITY_CLASS Nicht verfügbar. Wird als Parameter an die CreateProcess-API übergeben. Eingabe Die Prioritätsklasse, die der Prozess haben soll PS_CP_ERROR_MODE Nicht verfügbar. Wird über das Flag CREATE_DEFAULT_ ERROR_MODE übergeben. Eingabe Verarbeitungsmodus des Prozesses für harte Fehler PS_CP_STD_HANDLE_INFO Keines. Wird intern verwendet. Eingabe Gibt an, ob Standardhandles dupliziert oder ob neue Handles erstellt werden sollen. PS_CP_HANDLE_LIST PROC_THREAD_ATTRIBUTE_ HANDLE_LIST Eingabe Die Handles des Elternprozesses, die der neue Prozess erben soll PS_CP_GROUP_AFFINITY PROC_THREAD_ATTRIBUTE_ GROUP_AFFINITY Eingabe Die Prozessorgruppen, auf denen der Thread laufen darf PS_CP_PREFERRED_NODE PROC_THREAD_ATTRIBUTES_ PREFERRED_NODE Eingabe Der bevorzugte (ideale) NUMA-­ Knoten, der mit dem Prozess verknüpft werden soll. Dies legt den Knoten fest, auf dem der ursprüngliche Prozessheap und der ursprüngliche Threadstack erstellt werden (siehe Kapitel 5). → Der Ablauf von CreateProcess Kapitel 3 147
Natives Attribut Entsprechendes W32Attribut Typ Beschreibung PS_CP_IDEAL_​ PROCESSOR PROC_THREAD_ATTTRIBUTE_ IDEAL_PROCESSOR Eingabe Der bevorzugte (ideale) Prozessor, auf dem der Thread eingeplant werden soll PS_CP_UMS_THREAD PROC_THREAD_ATTRIBUTE_ UMS_THREAD Eingabe Enthält die UMS-Attribute, die Vervollständigungsliste und den Kontext. PS_CP_MITIGATION_­ OPTIONS PROC_THREAD_MITIGATION_ POLICY Eingabe Enthält Informationen über die vorbeugenden Maßnahmen (SEHOP, ATL-Emulation, NX), die für den Prozess aktiviert oder deaktiviert werden sollen. PS_CP_PROTECTION_LEVEL PROC_THREAD_ATTRIBUTE_ PROTECTION_LEVEL Eingabe Muss auf einen der zulässigen Prozessschutzwerte aus Tabelle 3–1 oder den Wert ­PROTECT_LEVEL_SAME zeigen. In letzterem Fall erhält der Prozess dieselbe Schutzebene wie sein Elternprozess. PS_CP_SECURE_PROCESS Keines. Wird intern verwendet. Eingabe Gibt an, dass der Prozess als IUM-Trustlet (Isolated User Mode) ausgeführt werden soll (siehe Kapitel 8 in Band 2). PS_CP_JOB_LIST Keines. Wird intern verwendet. Eingabe Weist den Prozess einer Jobliste zu. PS_CP_CHILD_PROCESS_ POLICY PROC_THREAD_ATTRIBUTE_ CHILD_PROCESS_POLICY Eingabe Gibt an, ob der neue Prozess direkt oder indirekt Kindprozesse erstellen darf (z. B. durch WMI). PS_CP_ALL_APPLICATION_PACKAGES_POLICY PROC_THREAD_ATTRIBUTE_ ALL_APPLICATION_­PACKAGES_ POLICY Eingabe Gibt an, ob das AppContainer-­ Token von Überprüfungen von Zugriffssteuerungslisten ausgenommen werden soll, die die Gruppe ALL ­APPLICATION PACKAGES enthalten. Stattdessen wird die Gruppe ALL ­RESTRICTED APPLICATION PACKAGES verwendet. PS_CP_WIN32K_FILTER PROC_THREAD_ATTRIBUTE_ WIN32K_FILTER Eingabe Gibt an, ob viele GDI/USER-Systemaufrufe des Prozesses an Win32k.sys gefiltert (blockiert) oder ob sie zugelassen, aber überwacht werden sollen. Wird vom Browser Microsoft Edge verwendet, um die Angriffsfläche zu verringern. PS_CP_SAFE_OPEN_ PROMPT_ORIGIN_CLAIM Keines. Wird intern verwendet. Eingabe Wird von »Mark of the Web« verwendet, um anzuzeigen, dass die Datei aus einer nicht vertrauenswürdigen Quelle stammt. 148 Kapitel 3 Prozesse und Jobs →
Natives Attribut Entsprechendes W32Attribut Typ Beschreibung PS_CP_BNO_ISOLATION PROC_THREAD_ATTRIBUTE_ BNO_ISOLATION Eingabe Sorgt dafür, dass das Primärtoken des Prozesses mit einem isolierten BaseNamedObjects-Verzeichnis verknüpft wird (siehe Kapitel 8 in Band 2). PS_CP_DESKTOP_APP_ POLICY PROC_THREAD_ATTRIBUTE_ DESKTOP_APP_POLICY Eingabe Gibt an, ob die moderne Anwendung ältere Desktopanwendungen starten darf und wenn ja, in welcher Weise. PROC_THREAD_­ ATTRIBUTE_SECURITY_­ CAPABILITIES Keines. Wird intern verwendet. Eingabe Gibt einen Zeiger auf eine ­SECURITY_CAPABILITIES-­ Struktur an, die dazu dient, ein AppContainer-Token für den Prozess zu erstellen, bevor ­NtCreateUserProcess aufgerufen wird. Tabelle 3–7 Prozessattribute 6. Fordert das Erstellungsflag eine getrennte virtuelle DOS-Maschine (VDM) an, obwohl der Prozess Teil eines Jobobjekts ist, so wird das Flag ignoriert. 7. Die an die CreateProcess-Funktion übergebenen Sicherheitsattribute für den Prozess und den ursprünglichen Thread werden in ihre interne Darstellung umgewandelt (OBJECT_ ATTRIBUTES-Strukturen; dokumentiert im WDK). 8. CreateProcessInternalW prüft, ob der Prozess als moderner Prozess erstellt werden soll. Das ist der Fall, wenn es in dem Attribut PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME mit dem vollständigen Paketnamen so verlangt wird oder wenn der Ersteller selbst ein moderner Prozess ist (und mit dem Attribut PROC_THREAD_ATTRIBUTE_PARENT_PROCESS nicht ausdrücklich ein Elternprozess angegeben wurde). In diesem Fall wird die interne Funktion BasepAppXExtension aufgerufen, um mehr Kontextinformationen über die Parameter der modernen App zu ermitteln. Sie befinden sich in der Struktur APPX_PROCESS_CONTEXT, die Informationen wie den Paketnamen (intern als »package moniker« bezeichnet), die Fähigkeiten der Anwendung, das aktuelle Verzeichnis für den Prozess und die Angabe enthält, ob der App volles Vertrauen geschenkt werden soll. Die Möglichkeit, eine voll vertrauenswürdige moderne App zu erstellen, ist nicht öffentlich verfügbar, sondern für Anwendungen reserviert, die zwar über ein modernes Erscheinungsbild verfügen, aber Operationen auf Systemebene durchführen. Ein wichtiges Beispiel ist die App Einstellungen in Windows 10 (SystemSettings.exe). 9. Wird ein moderner Prozess erstellt, so wird die interne Funktion BasepCreateLowBox aufgerufen, um die Sicherheitsfähigkeiten (sofern in PROC_THREAD_ATTRIBUTE_SECURITY_ CAPABILITIES bereitgestellt) für die Erstellung des ursprünglichen Tokens aufzuzeichnen. Die Bezeichnung LowBox bezieht sich auf die Sandbox (den Anwendungscontainer), in der der Prozess ausgeführt wird. Es ist zwar nicht möglich, moderne Prozesse direkt durch einen Aufruf von CreateProcess zu erstellen (stattdessen müssen die zuvor beschriebenen COM-Schnittstellen verwendet werden), doch im Windows SDK und auf MSDN wird die Der Ablauf von CreateProcess Kapitel 3 149
Möglichkeit beschrieben, ältere AppContainer-Desktopanwendungen durch die Übergabe dieses Attributs zu erstellen. 10. Soll ein moderner Prozess erstellt werden, so wird ein Flag gesetzt, um anzuzeigen, dass der Kernel die Erkennung des eingebetteten Manifests überspringen soll. Moderne Prozesse sollten kein eingebettetes Manifest haben, da es nicht benötigt wird. (Eine moderne App hat ihr eigenes Manifest, das aber nichts mit dem hier gemeinten eingebetteten Manifest zu tun hat.) 11. Wurde das Debugflag DEBUG_PROCESS angegeben, wird der Debugger-Wert im Registrierungsschlüssel Image File Execution Options für die ausführbare Datei (siehe den nächsten Abschnitt) so markiert, dass er übersprungen wird. Anderenfalls könnte ein Debugger niemals den zu debuggenden Prozess erstellen, da der Vorgang in eine Endlosschleife eintritt (es wird immer und immer wieder versucht, den Debuggerprozess anzulegen). 12. Alle Fenster werden mit Desktops verknüpft, den grafischen Darstellungen eines Arbeitsbereichs. Ist in der STARTUPINFO-Struktur kein Desktop angegeben, so wird der Prozess mit dem aktuellen Desktop des Aufrufers verknüpft. Die Windows 10-Funktion der virtuellen Desktops verwendet in Wirklichkeit nicht mehrere Desktopobjekte (im Sinne von Kernelobjekten). Es gibt immer noch lediglich einen Desktop, auf dem aber die Fenster nach Bedarf angezeigt bzw. ausgeblendet werden. Anders ist es beim Sysinternals-Tools Desktops.exe, das wirklich bis zu vier Desktopobjekte erstellt. Den Unterschied können Sie erkennen, wenn Sie versuchen, ein Fenster von einem Desktop zu einem anderen zu verschieben. Bei Desktops.exe geht das nicht, da eine solche Operation in Windows nicht unterstützt wird. In den virtuellen Desktops von Windows 10 ist es jedoch möglich, da dabei in Wirklichkeit gar nichts verschoben wird. 13. Die an CreateProcessInternalW übergebene Anwendung und die Befehlszeilenargumente werden analysiert. Der Pfad der ausführbaren Datei wird in den internen NT-Namen umgewandelt (beispielsweise wird aus c:\temp\a.exe etwas wie \device\harddiskvolume1\ temp\a.exe), da einige Funktionen auf dieses Format angewiesen sind. 14. Die meisten erfassten Informationen werden in eine einzige große Struktur vom Typ RTL_ USER_PROCESS_PARAMETERS umgewandelt. Nach Abschluss dieser Schritte ruft CreateProcessInternalW die Funktion NtCreateUserProcess auf, um zu versuchen, den Prozess zu erstellen. Da Kernel32.dll zu diesem Zeitpunkt noch nicht weiß, ob der Name des Abbilds zu einer echten Windows-Anwendung oder zu einer Batchdatei (.bat oder .cmd), einer 16-Bit- oder DOS-Anwendung gehört, kann dieser Aufruf fehlschlagen. In diesem Fall schaut sich CreateProcessInternalW den Grund für den Fehler an und versucht, ihn zu korrigieren. 150 Kapitel 3 Prozesse und Jobs
Phase 2: Das auszuführende Abbild öffnen An dieser Stelle ist der erstellende Thread in den Kernelmodus gewechselt und fährt mit seiner Arbeit in der Implementierung des Systemaufrufs NtCreateUserProcess fort. 1. Als Erstes validiert NtCreateUserProcess die Argumente und baut eine interne Struktur für alle Informationen zu dem Erstellungsvorgang auf. Validiert werden die Argumente, um sicherzugehen, dass der Aufruf an die Exekutive nicht von einem Hack ausging, der den Übergang von Ntdll.dll zum Kernel mit gefälschten oder schädlichen Argumenten simuliert. 2. Wie Abb. 3–11 zeigt, besteht der nächste Schritt darin, das geeignete Windows-Abbild zu finden, um die vom Aufrufer angegebene ausführbare Datei auszuführen und ein Abschnittsobjekt zu erstellen, das später im Adressraum des neuen Prozesses abgebildet wird. Wenn dieser Aufruf aus irgendeinem Grund fehlschlägt, gibt er einen Fehlerstatus an CreateProcessInternalW zurück (siehe Tabelle 3–8). Mithilfe dieser Informationen versucht CreateProcessInternalW es dann noch einmal. Führt Cmd.exe aus Führt NtVdm.exe aus BAT- oder CMDDatei von DOS Win16 Führt die EXE-Datei direkt aus Windows Um was für eine Anwendung handelt es sich? EXE-, COM- oder PIF-Datei von DOS Führt NtVdm.exe aus Abbildung 3–11 Auswahl des zu verwendenden Windows-Abbilds 3. Soll ein geschützter Prozess erstellt werden, so wird auch die Signierrichtlinie geprüft. 4. Soll ein moderner Prozess erstellt werden, so erfolgt eine Lizenzierungsprüfung, um sicherzugehen, dass der Prozess lizenziert ist und ausgeführt werden darf. Anwendungen, die zusammen mit Windows vorinstalliert wurden, dürfen unabhängig von der Lizenz immer ausgeführt werden. Ist das Querladen zugelassen (was über Einstellungen festgelegt wird), dann können alle signierten Anwendungen ausgeführt werden, nicht nur diejenigen aus dem Store. 5. Handelt es sich bei dem Prozess um ein Trustlet, so muss das Abschnittsobjekt mit einem besonderen Flag erstellt werden, das die Verwendung durch den sicheren Kernel erlaubt. 6. Wenn es sich bei der angegebenen ausführbaren Datei um eine Windows-EXE handelt, versucht NtCreateUserProcess, sie zu öffnen und ein Abschnittsobjekt dafür zu erstellen. Der Ablauf von CreateProcess Kapitel 3 151
Dieses Objekt wird noch nicht auf den Arbeitsspeicher abgebildet, aber es ist geöffnet. Die Tatsache, dass ein Abschnittsobjekt erstellt werden konnte, bedeutet jedoch nicht zwangsläufig, dass es sich bei der Datei um ein gültiges Windows-Abbild handelt, denn es könnte auch eine DLL oder eine ausführbare POSIX-Datei sein. In letzterem Fall schlägt der Aufruf fehl, da POSIX nicht mehr unterstützt wird. Bei einer DLL schlägt CreateProcessInternalW ebenfalls fehl. 7. Nachdem NtCreateUserProcess ein gültiges Windows-Abbild gefunden hat, schaut sie in der Registrierung unter HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options nach, ob es einen Unterschlüssel mit dem Dateinamen und der Erweiterung des Abbilds gibt (allerdings ohne Verzeichnis- und Pfadangaben, also z. B. Notepad. exe). Wenn ja, sucht PspAllocateProcess nach dem Debugger-Wert für diesen Schlüssel. Ist er vorhanden, wird der Name des auszuführenden Abbilds zum String für diesen Wert. CreateProcessInternalW startet dann erneut in Phase 1. Sie können dieses Verhalten nutzen, um den Startcode von Windows-Dienstprozessen zu debuggen, bevor sie gestartet werden. Wenn Sie den Debugger erst nach dem Start einklinken, ist ein Debugging des Startcodes nicht mehr möglich. 8. Handelt es sich bei dem Abbild dagegen nicht um eine Windows-EXE (sondern z. B. um eine MS-DOS- oder eine Win16-Anwendung), durchläuft CreateProcessInternalW eine Reihe von Schritten, um ein Windows-Unterstützungsabbild zur Ausführung dieser Datei zu finden. Dieser Vorgang ist notwendig, da Nicht-Windows-Anwendungen nicht direkt ausgeführt werden. Stattdessen verwendet Windows dazu besondere Unterstützungsabbilder, die letzten Endes für die Ausführung des Nicht-Windows-Programms verantwortlich sind. Wenn Sie beispielsweise (auf 32-Bit-Windows) versuchen, eine ausführbare MS-DOS- oder Win16-Datei zu öffnen, wird das Abbild Ntvdm.exe ausgeführt. Kurz: Es ist nicht möglich, einen Nicht-Windows-Prozess direkt zu erstellen. Wenn Windows keine Möglichkeit findet, das angegebene Abbild auszuführen, schlägt CreateProcessInternalW fehlt (siehe Tabelle 3–8). Abbild Statuscode Ausgeführtes Abbild Ergebnis MS-DOS-Anwendung mit der Erweiterung .exe, .com oder .pif PsCreateFailOnSectionCreate Ntvdm.exe CreateProcessInternalW startet neu in Phase 1. Win16-Anwendung PsCreateFailOnSectionCreate Ntvdm.exe CreateProcessInternalW startet neu in Phase 1. Win64-Anwendung auf einem 32-Bit-System (oder eine PPC-, MIPS- oder Alpha-Binärdatei) PsCreateFailMachineMismatch – CreateProcessInternalW schlägt fehl. 152 Kapitel 3 → Prozesse und Jobs
Abbild Statuscode Ausgeführtes Abbild Ergebnis Hat einen Debugger-Wert mit einem anderen Abbildnamen PsCreateFailExeName Im Debugger-Wert angegebener Name CreateProcessInternalW startet neu in Phase 1. Ungültige oder beschädigte Windows-EXE PsCreateFailExeFormat – CreateProcessInternalW schlägt fehl. Kann nicht geöffnet werden PsCreateFailOnFileOpen – CreateProcessInternalW schlägt fehl. Befehlsprozedur (Anwendung mit der Erweiterung .bat oder .com) PsCreateFailOnSectionCreate Cmd.exe CreateProcessInternalW startet neu in Phase 1. Tabelle 3–8 Umgang mit verschiedenen Abbildern CreateProcessInternalW entscheidet wie folgt, welches Abbild sie ausführen soll: QQ Wenn x86-32-Bit-Windows ausgeführt wird und es sich bei dem Abbild um eine MS-DOS-Anwendung mit der Erweiterung .exe, .com oder .pif handelt, wird eine Nachricht an das Windows-Teilsystem gesendet, um zu prüfen, ob für die Sitzung bereits ein MS-DOS-Unterstützungsprozess erstellt worden ist (nämlich Ntvdm.exe, wie im Registrierungswert HKLM\SYSTEM\CurrentControlSet\Control\WOW\cmdline angegeben). Wenn ja, wird dieser Unterstützungsprozess verwendet, um die MS-DOS-Anwendung auszuführen. (Das Windows-Teilsystem sendet die Nachricht an den VDM-Prozess [virtuelle DOS-Maschine], um das neue Abbild auszuführen.) Danach gibt CreateProcessInternalW die Steuerung zurück. Wurde dagegen noch kein Unterstützungsprozess erstellt, wird als auszuführendes Image Ntvdm.exe angegeben, woraufhin CreateProcessInternalW mit Phase 1 neu beginnt. QQ Trägt die auszuführende Datei die Endung .bat oder .cmd, wird die Windows-Eingabeaufforderung Cmd.com zum auszuführenden Abbild, woraufhin CreateProcessInternalW mit Phase 1 neu beginnt. Der Name der Batchdatei wird als zweiter Parameter nach dem Schalter /c an Cmd.exe übergeben. QQ Findet der Vorgang auf einem x86-Windows-System statt und handelt es sich bei dem Abbild um eine ausführbare Win16-Datei (Windows 3.1), muss CreateProcessInternalW entscheiden, ob ein neuer VDM-Prozess dafür erstellt oder der standardmäßige sitzungsweit gemeinsam genutzte VDM-Prozess verwendet werden soll (der möglicherweise noch nicht erstellt wurde). Für diese Entscheidung werden die CreateProcess-Flags CREATE_SEPARATE_ WOW_VDM und CREATE_SHARED_WOW_VDM herangezogen. Wenn sie nicht angegeben sind, gibt der Registrierungswert HKLM\SYSTEM\CurrentControlSet\Control\WOW\DefaultSeparateVDM das Standardverhalten vor. Soll die Anwendung in einer eigenen VDM ausgeführt werden, wird das auszuführende Abbild in Ntvdm.exe gefolgt von einigen Konfigurationsparametern und dem Namen des 16-Bit-Prozesses geändert, woraufhin CreateProcessInternalW in Phase 1 neu startet. Anderenfalls sendet das Windows-Teilsystem eine Nachricht, um zu prüfen, ob der gemeinsam genutzte VDM-Prozess vorhanden ist und verwendet werden Der Ablauf von CreateProcess Kapitel 3 153
kann. Läuft der VDM-Prozess auf einem anderen Desktop oder in einem anderen Sicherheitskontext als dem des Aufrufers, dann kann er nicht genutzt werden. In diesem Fall muss ein neuer VDM-Prozess erstellt werden. Kann der gemeinsame VDM-Prozess dagegen verwendet werden, dann sendet das Windows-Teilsystem ihm eine Nachricht, damit er das neue Abbild ausführt. CreateProcessInternalW gibt dann die Steuerung zurück. Wenn der VDM-Prozess noch nicht gestartet wurde (oder nicht genutzt werden kann), wird das auszuführende Abbild in das VDM-Unterstützungsabbild geändert, woraufhin CreateProcessInternalW in Phase 1 neu startet. Phase 3: Das Windows-Exekutivprozessobjekt erstellen NtCreateUserProcess hat jetzt eine gültige ausführbare Windows-Datei geöffnet und ein Ab- schnittsobjekt erstellt, um sie auf den Adressraum des neuen Prozesses abzubilden. Als Nächstes erstellt die Funktion ein Windows-Exekutivprozessobjekt, um das Abbild auszuführen. Dazu ruft sie die interne Systemfunktion PspAllocateProcess auf. Die Erstellung des Exekutivprozessobjekts (die vom Erstellerthread erledigt wird) umfasst die folgenden Teilphasen: a. Das EPROCESS-Objekt einrichten b. Den ursprünglichen Prozessadressraum erstellen c. Die KPROCESS-Struktur initialisieren d. Die Einrichtung des Prozessadressraums abschließen e. Den PEB einrichten f. Die Einrichtung des Exekutivprozessobjekts abschließen Nur während des Systeminitialisierens (beim Erstellen des Systemprozesses) gibt es keinen Elternprozess. Danach ist immer ein Elternprozess erforderlich, um den Sicherheitskontext für den neuen Prozess bereitzustellen. Phase 3a: Das EPROCESS-Objekt einrichten Diese Teilphase umfasst die folgenden Schritte: 1. Der neue Prozess erbt die Affinität des Elternprozesses, es sei denn, während der Prozess­ erstellung wurde in der Attributliste ausdrücklich eine Affinität angegeben. 2. Gegebenenfalls wird der NUMA-Knoten ausgewählt, der als idealer Knoten in der Attributliste angegeben wurde. 3. Der neue Prozess erbt die E/A- und Seitenpriorität vom Elternprozess. Gibt es keinen Elternprozess, werden die Standardprioritäten verwendet, also 5 für die Seiten- und Normal für die E/A-Priorität. 4. Der neue Prozessbeendigungsstatus wird auf STATUS_PENDING gesetzt. 5. Es wird der Verarbeitungsmodus für harte Fehler ausgewählt, der in der Attributliste angegeben ist. Wird dort keiner genannt, so erbt der Prozess den Modus des Elternprozesses. 154 Kapitel 3 Prozesse und Jobs
Gibt es keinen Elternprozess, wird der Standardverarbeitungsmodus genommen, bei dem alle Fehler angezeigt werden. 6. Die ID des Elternprozesses wird im Feld InheritedFromUniqueProcessID des neuen Prozessobjekts gespeichert. 7. Der Schlüssel Image File Execution Objects (IFEO) wird abgefragt, um herauszufinden, ob der Prozess auf großen Seiten abgebildet werden soll (Wert UseLargePages). Wenn der Prozess unter Wow64 läuft, werden jedoch grundsätzlich keine großen Seiten verwendet. Es wird auch geprüft, ob in diesem Schlüssel NTDLL als die DLL angegeben ist, die in dem Prozess auf großen Seiten abgebildet werden soll. 8. Der Leistungsoptionsschlüssel PerfOptions in IFEO wird abgefragt (falls vorhanden). Er kann mehrere der folgenden Werte enthalten: IoPriority, PagePriority, CpuPriorityClass und WorkingSetLimitInKB. 9. Soll der Prozess unter Wow64 laufen, dann wird die Wow64-Hilfsstruktur EWOW64PROCESS zugewiesen und im Element WoW64Process der EPROCESS-Struktur angegeben. 10. Soll der Prozess in einem Anwendungscontainer laufen (was bei einer modernen App meistens der Fall ist), wird das mit LowBox erstellte Token validiert (siehe Kapitel 7). 11. Es wird versucht, alle erforderlichen Rechte zum Erstellen des Prozesses zu erwerben. Die Auswahl der Prioritätsklasse Echtzeit, die Zuweisung eines Tokens zu dem neuen Prozess, die Abbildung des Prozesses auf großen Seiten und das Erstellen des Prozesses in einer neuen Sitzung erfordern jeweils entsprechende Rechte. 12. Das primäre Zugriffstoken des Prozesses (ein Duplikat des Primärtokens des Elternprozesses) wird erstellt. Neue Prozesse erben das Sicherheitsprofil ihrer Elternprozesse. Wird mit CreateProcessAsUser ein anderes Zugriffstoken für den neuen Prozess angegeben, so wird das Token entsprechend geändert. Das kann jedoch nur dann geschehen, wenn das Token des übergeordneten Prozesses eine höhere Integritätsebene aufweist als das Zugriffstoken des neuen Prozesses und wenn dieses Zugriffstoken ein echtes Kind oder Geschwister des Elterntokens ist. Hat der Elternprozess das Recht SeAssignPrimaryToken, wird diese Überprüfung übersprungen. 13. Die Sitzungs-ID des neuen Prozesstokens wird geprüft, um zu bestimmen, ob der neue Prozess sitzungsübergreifend erstellt wird. Wenn ja, so wird der Elternprozess vorübergehend an die Zielsitzung angehängt, um die Kontingente und die Adressraumerstellung korrekt zu verarbeiten. 14. Der Kontingentblock des neuen Prozesses wird auf die Adresse des Elternkontingentblocks gesetzt. Der Referenzzähler für den Elternkontingentblock wird heraufgesetzt. Wurde der Prozess mit CreateProcessAsUser erstellt, wird dieser Schritt nicht ausgeführt. Stattdessen wird das Standardkontingent erstellt oder das Kontingent aus dem Benutzerprofil ausgewählt. 15. Die Mindest- und Höchstgröße des Arbeitssatzes für den Prozess werden auf die Werte von PspMinimumWorkingSet bzw. PspMaximumWorkingSet gesetzt. Diese Werte können überschrieben werden, wenn im IFEO-Schlüssel PerfOptions entsprechende Leistungsoptionen angegeben wurden. In diesem Fall wird die maximale Arbeitssatzgröße aus diesem Schlüs- Der Ablauf von CreateProcess Kapitel 3 155
sel bezogen. Die Standardgrenzwerte für den Arbeitssatz sind im Grunde genommen nur Empfehlungen, während es sich bei dem Maximalwert in PerfOptions um eine feste Grenze handelt, die der Arbeitssatz nicht überschreiten kann. 16. Der Adressraum des Prozesses wird initialisiert (siehe Phase 3b). Wird der neue Prozess in einer anderen Sitzung erstellt, so wird der Elternprozess jetzt von der Zielsitzung getrennt. 17. Wurde die Gruppenaffinität für den Prozess nicht geerbt, so wird sie jetzt ausgewählt. Die Standardgruppenaffinität wird entweder vom Elternprozess geerbt, wenn zuvor die NUMA-Knotenweiterleitung eingerichtet wurde (dabei wird die Gruppe verwendet, die den NUMA-Knoten besitzt), oder mit einem Umlaufverfahren zugewiesen. Befindet sich das System in einem erzwungenen Gruppenkenntnismodus und hat sich der Auswahlalgorithmus für Gruppe 0 entschieden, so wird stattdessen Gruppe 1 ausgewählt, falls sie vorhanden ist. 18. Der KPROCESS-Teil des Prozessobjekts wird initialisiert (siehe Phase 3c). 19. Das Token für den Prozess wird jetzt eingerichtet. 20. Die Prioritätsklasse des Prozesses wird auf Normal gesetzt. Hatte der Elternprozess die Klasse Leerlauf oder Niedriger als normal, so wird stattdessen diese Priorität verwendet. 21. Die Prozesshandletabelle wird initialisiert. Ist das Flag zur Handlevererbung für den Elternprozess gesetzt, werden alle vererbbaren Handles von dessen Objekthandletabelle in die des neuen Prozesses kopiert (mehr darüber siehe Kapitel 8 in Band 2). Mit einem Prozessattribut kann jedoch auch nur eine Teilmenge der Handles angegeben werden. Das ist praktisch, wenn Sie mit CreateProcessAsUser einschränken wollen, welche Objekte der Kindprozess erben soll. 22. Wurden im Schlüssel PerfOptions Leistungsoptionen angegeben, so werden diese jetzt angewendet. Dieser Schlüssel enthält Werte, mit denen die Grenzwerte für den Arbeitssatz, die E/A-, die Seiten- und die CPU-Priorität des Prozesses überschrieben werden können. 23. Die endgültige Prioritätsklasse für den Prozess und das Standardquantum für seine Threads werden berechnet und festgelegt. 24. Die verschiedenen Vorbeugungsoptionen im IFEO-Schlüssel (ein einzelner 64-Bit-Wert namens Mitigation) werden gelesen und festgelegt. Läuft der Prozess in einem Anwendungscontainer, wird das Abwehrflag TreatAsAppContainer hinzugefügt. 25. Alle anderen Vorbeugungsflags werden angewendet. Phase 3b: Den ursprünglichen Prozessadressraum erstellen Der ursprüngliche Prozessadressraum besteht aus den folgenden Seiten: QQ Seitenverzeichnis (auf Systemen mit Seitentabellen, die mehr als zwei Ebenen umfassen, kann es mehr als ein Verzeichnis geben, beispielsweise auf x86-Systemen im PAE-Modus und auf 64-Bit-Systemen) QQ Hyperspace-Seite QQ VAD-Bitmap-Seite QQ Arbeitssatzliste 156 Kapitel 3 Prozesse und Jobs
Um diese Seiten zu erstellen, werden die folgenden Schritte ausgeführt: 1. 2. In den entsprechenden Seitentabellen werden Einträge vorgenommen, um die ursprünglichen Seiten abzubilden. Die Anzahl der Seiten wird aus der Kernelvariablen MmTotalCommittedPages ermittelt und zu MmProcessCommit addiert. 3. Die systemweite Mindestgröße für Arbeitssätze von Standardprozessen (PsMinimumWorkingSet) wird aus MmResidentAvailablePages ermittelt. 4. Die Seitentabellenseiten für den globalen Systemraum (also alle außer den gerade beschriebenen prozessspezifischen Seiten und dem sitzungsspezifischen Arbeitsspeicher) werden erstellt. Phase 3c: Die KPROCESS-Struktur initialisieren Die nächste Phase von PspAllocateProcess besteht darin, die KPROCESS-Struktur zu initialisieren (das Element Pcb in EPROCESS). Diese Aufgabe erledigt KeInitializeProcess wie folgt: 1. Die doppelt verlinkte Liste, die alle zu dem Prozess gehörenden Threads verbindet und ursprünglich leer ist, wird initialisiert. 2. Der Anfangswert (oder Rücksetzwert) des Standardquantums für den Prozess (siehe den Abschnitt »Threadplanung« in Kapitel 4) wird als 6 hartcodiert, bis er später von PspComputeQuantumAndPriority initialisiert wird. Das ursprüngliche Standardquantum ist in den Client- und den Serverversionen von Windows unterschiedlich. Weitere Angaben dazu erhalten Sie im Abschnitt »Threadplanung« in Kapitel 4. 3. Die Basispriorität des Prozesses wird auf den in Phase 3a berechneten Wert gesetzt. 4. Die Standardprozessoraffinität für die Threads in dem Prozess wird festgelegt. Die Gruppenaffinität wird auf den in Phase 3a berechneten oder auf den vom Elternprozess geerbten Wert gesetzt. 5. Der Swapping-Status für den Prozess wird auf resident gesetzt. 6. Der Thread-Seed wird anhand des idealen Prozessors bestimmt, den der Kernel für den Prozess ausgewählt hat (und der wiederum auf dem idealen Prozessor des zuvor erstellten Prozesses basiert, was letzten Endes zu einer Randomisierung in der Art eines Umlaufverfahrens führt). Beim Erstellen eines neuen Prozesses wird der Seed in KeNodeBlock (dem ursprünglichen NUMA-Knotenblock) aktualisiert, sodass der nächste neue Prozessor einen anderen idealen Prozessor für den Seed erhält. 7. Bei einem sicheren Prozess (Windows 10 und Windows Server 2016) wird jetzt die sichere ID durch einen Aufruf von HvlCreateSecureProcess erstellt. Der Ablauf von CreateProcess Kapitel 3 157
Phase 3d: Die Einrichtung des Prozessadressraums abschließen Die Einrichtung des Adressraums für einen neuen Prozess ist recht kompliziert, weshalb wir uns diesen Vorgang Schritt für Schritt ansehen. Für das Verständnis dieses Abschnitts sind Kenntnisse des in Kapitel 5 beschriebenen Speicher-Managers von Nutzen. Den Großteil der Arbeit zum Einrichten des Adressraums erledigt die Routine MmInitializeProcessAddressSpace. Sie ermöglicht es auch, den Adressraum eines anderen Prozesses zu klonen. Diese Möglichkeit war früher praktisch, um den POSIX-Systemaufruf fork zu implementieren, und kann sich in Zukunft vielleicht auch wieder als nützlich erweisen, um andere fork-Aufrufe nach Art von Unix zu unterstützen. (Auf diese Weise wird fork im Windows-Teilsystem für Linux in Redstone 1 implementiert.) In den folgenden Schritten wird dieser Klonvorgang jedoch nicht beschrieben, sondern die normale Initialisierung des Prozessadressraums: 1. Der Manager für den virtuellen Speicher setzt den Wert für den letzten Kürzungszeitpunkt des Prozesses auf die aktuelle Zeit. Der Arbeitssatz-Manager (der im Kontext des Systemthreads für den Ausgleichssatz-Manager läuft) bestimmt anhand dieses Wertes, wann er die Kürzung des Arbeitssatzes auslösen soll. 2. Der Speicher-Manager initialisiert die Arbeitssatzliste des Prozesses. Jetzt können Seitenfehler entgegengenommen werden. 3. Der Abschnitt (der beim Öffnen der Abbilddatei erstellt wurde) wird jetzt im Adressraum des neuen Prozesses abgebildet. Die Basisadresse des Prozessabschnitts wird auf die Basis­ adresse des Abbilds gesetzt. 4. Der Prozessumgebungsblock (PEB) wird erstellt und initialisiert (siehe Phase 3e). 5. Ntdll.dll wird dem Prozess zugeordnet, und bei einem Wow64-Prozess auch die 32-Bit-Version von Ntdll.dll. 6. Falls verlangt, wird eine neue Sitzung für den Prozess erstellt. Dieser Schritt ist hauptsächlich für den Sitzungs-Manager (Smss) bei der Initialisierung einer neuen Sitzung da. 7. Die Standardhandles werden dupliziert und die neuen Werte werden in die Prozessparameterstruktur geschrieben. 8. Alle in der Attributliste aufgeführten Speicherreservierungen werden jetzt verarbeitet. Zwei Flags erlauben auch eine Massenreservierung der ersten 1 oder 16 MB des Adressraums. Diese Flags werden intern verwendet, um beispielsweise Realmodusvektoren und ROMCode zuzuordnen (die sich im unteren Bereich des virtuellen Adressraums befinden müssen, wo normalerweise der Heap oder andere Prozessstrukturen angeordnet werden). 9. Die Benutzerprozessparameter werden in den Prozess geschrieben, kopiert und von der absoluten in eine relative Form umgewandelt, sodass nur ein einziger Speicherblock dafür erforderlich ist. 10. Die Affinitätsinformationen werden in den PEB geschrieben. 11. Der API-Umleitungssatz MinWin wird in dem Prozess abgebildet. Sein Zeiger wird im PEB gespeichert. 12. Die eindeutige Prozess-ID wird ermittelt und gespeichert. Der Kernel unterscheidet nicht zwischen eindeutigen Prozess- und Thread-IDs und Handles. Prozess- und Thread-IDs 158 Kapitel 3 Prozesse und Jobs
(Handles) werden in der globalen Handletabelle PspCidTable gespeichert, die nicht mit einem Prozess verknüpft ist. 13. Wenn es sich um einen sicheren Prozess handelt (wenn er also im IUM läuft), so wird er initialisiert und mit dem Kernelprozessobjekt verknüpft. Phase 3e: Den PEB einrichten NtCreateUserProcess ruft MmCreatePeb auf, die als Erstes die systemweiten Sprachunterstüt- zungstabellen (National Language Support, NLS) in den Prozessadressraum abbildet. Anschließend ruft sie MiCreatePeb=rTeb auf, um eine Seite für den PEB zuzuweisen, und initialisiert dann eine Reihe von Feldern, größtenteils auf der Grundlage von internen Variablen, die über die Registrierung eingestellt wurden, z. B. MmHeap*-Werte, MmCriticalSectionTimeout und MmMinimumStackCommitInBytes. Einige dieser Felder können durch Einstellungen im verlinkten ausführbaren Image überschrieben werden, z. B. die Windows-Version im PE-Header und die Affinitätsmaske in dessen Ladekonfigurationsverzeichnis. Ist das Abbildheader-Flag IMAGE_FILE_UP_SYSTEM_ONLY gesetzt (was bedeutet, dass das Abbild nur auf einem Einzelprozessorsystem ausgeführt werden kann), so wird für alle Threads in diesem Prozess eine einzige CPU ausgewählt (MmRotatingUniprocessorNumber). Die Auswahl erfolgt einfach durch Durchlaufen aller verfügbaren Prozessoren. Jedes Mal, wenn ein Abbild dieser Art ausgeführt wird, kommt der nächste Prozessor an die Reihe. Dadurch werden solche Abbilder gleichmäßig auf die Prozessoren verteilt. Phase 3f: Die Einrichtung des Exekutivprozessobjekts abschließen Bevor das Handle für den neuen Prozess zurückgegeben werden kann, müssen noch einige letzte Einrichtungsschritte abgeschlossen werden. Ausgeführt werden sie von PspInsertProcess und ihren Hilfsfunktionen: 1. Ist die systemweite Prozessüberwachung aktiviert (aufgrund einer lokalen Richtlinieneinstellung oder einer Gruppenrichtlinieneinstellung auf einem Domänencontroller), so wird das Erstellen des Prozesses im Sicherheitsprotokoll vermerkt. 2. War der Elternprozess in einem Job enthalten, so wird dieser Job aus dem Jobsatz des Elternprozesses wiederhergestellt und an die Sitzung des neuen Prozesses gebunden. Schließlich wird der neue Prozess zu dem Job hinzugefügt. 3. Das neue Prozessobjekt wird am Ende der Windows-Liste aktiver Prozesse eingefügt (PsActiveProcessHead). Jetzt ist der Prozess für Funktionen wie EnumProcess und OpenProcess zugänglich. 4. Der Prozessdebugport des Elternprozesses wird in den neuen Kindprozess kopiert, es sei denn, das Flag NoDebugInherit ist gesetzt (das beim Erstellen des Prozesses angefordert werden kann). Wurde ein Debugport angegeben, so wird dieser mit dem neuen Prozess verknüpft. 5. Jobobjekte können die möglichen Gruppen beschränken, in denen die Threads in den Prozessen des Jobs ausgeführt werden können. Daher vergewissert sich PspInsertProcess, dass die Gruppenaffinität des Prozesses nicht die Gruppenaffinität des Jobs verletzt. Ein Der Ablauf von CreateProcess Kapitel 3 159
weiteres interessantes Problem liegt vor, wenn die Berechtigungen des Jobs es erlauben, die Affinitätsberechtigungen des Prozesses zu ändern, da ein Jobobjekt mit geringeren Rechten dann die Affinitätsanforderungen eines höher privilegierten Prozesses manipulieren könnte. 6. Schließlich ruft PspInsertProcess die Funktion ObOpenObjectByPointer auf, um ein Handle für den neuen Prozess zu erstellen, und gibt dieses Handle dann an den Aufrufer zurück. Es wird erst dann ein Prozesserstellungscallback gesendet, wenn der erste Thread in dem Prozess angelegt wurde. Der Code sendet Prozesscallbacks immer vor Callbacks zur Objektverwaltung. Phase 4: Den ursprünglichen Thread mit seinem Stack und Kontext erstellen Das Windows-Exekutivprozessobjekt ist jetzt fast vollständig eingerichtet. Es hat jedoch noch keinen Thread und kann daher nichts tun. Für alle Aspekte der Threaderstellung ist normalerweise die Routine PspCreateThread zuständig, die von NtCreateThread aufgerufen wird, um einen neuen Thread anzulegen. Da der ursprüngliche Thread jedoch intern vom Kernel ohne Eingaben vom Benutzermodus erstellt wird, kommen stattdessen die beiden Hilfsroutinen PspAllocateThread und PspInsertThread zum Einsatz, auf die sich auch PspCreateThread stützt. Dabei kümmert sich PspAllocateThread darum, das Exekutivthreadobjekt zu erstellen und zu initialisieren, während PspInsertHandle den Threadhandle und die Sicherheitsattribute anlegt und KeStartThread aufruft, um aus dem Exekutivobjekt einen planbaren Thread zu machen. Allerdings tut der Thread danach immer noch nichts, da er in angehaltenem Zustand erstellt wird. Er wird erst wieder aufgenommen, wenn der Prozess vollständig initialisiert ist (siehe Phase 5). Der Threadparameter (der nicht in CreateProcess, sondern in CreateThread gesetzt werden kann) gibt die Adresse des PEB an. Verwendet wird dieser Parameter von dem Initialisierungscode, der im Kontext des neuen Threads ausgeführt wird (siehe Phase 6). PspAllocateThread führt die folgenden Schritte aus: 1. Sie verhindert, dass in Wow64-Prozessen UMS-Threads (User-Mode Scheduling) erstellt werden und dass Aufrufer aus dem Benutzermodus Threads in Systemprozessen anlegen. 2. Sie erstellt und initialisiert ein Exekutivthreadobjekt. 3. Sind für das System Energieabschätzungen aktiviert (auf der Xbox sind sie immer deaktiviert), weist sie die THREAD_ENERGY_VALUES-Struktur zu, auf die das ETHREAD-Objekt zeigt, und initialisiert sie. 4. Sie initialisiert die Listen, die von LPC, der E/A-Verwaltung und der Exekutive verwendet werden. 5. Sie legt die Threaderstellungszeit fest und richtet die Thread-ID ein. 160 Kapitel 3 Prozesse und Jobs
6. Bevor der Thread ausgeführt werden kann, braucht er einen Stack und einen Ausführungskontext, die jetzt von der Routine eingerichtet werden. Die Stackgröße für den ursprünglichen Thread wird aus dem Abbild bezogen. Es gibt keine Möglichkeit, eine andere Größe anzugeben. Bei einem Wow64-Prozess wird auch der Wow64-Threadkontext initialisiert. 7. Sie weist den Threadumgebungsblock (TEB) für den neuen Thread zu. 8. Die Benutzermodus-Startadresse für den Thread wird im Feld StartAddress der ETHREAD-Struktur gespeichert. Dies ist die vom System bereitgestellte Threadstartfunktion RtlUserThreadStart aus Ntdll.dll. Die vom Benutzer angegebene Windows-Startadresse wird ebenfalls in der ETHREAD-Struktur gespeichert, aber an anderer Stelle, nämlich im Feld Win32StartAddress. Dadurch sind Debuggingwerkzeuge wie Process Explorer in der Lage, diese Information anzuzeigen. 9. Um die KTHREAD-Struktur einzurichten, wird KeInitThread aufgerufen. Die ursprüngliche und die aktuelle Basispriorität des Threads, seine Affinität und sein Quantum werden auf die Basispriorität des Prozesses gesetzt. Danach weist KeInitThread einen Kernelstack für den Thread zu und initialisiert den maschinenabhängigen Hardwarekontext für den Thread einschließlich Kontext-, Trap- und Ausnahmeframes. Der Threadkontext wird so eingerichtet, dass der Thread in KiThreadStartup im Kernelmodus gestartet wird. Schließlich setzt KeInitThread den Threadstatus auf Initialized und gibt die Steuerung an PspAllocateThread zurück. 10. Bei einem UMS-Thread wird PspUmsInitThread aufgerufen, um den UMS-Status zu initialisieren. Nachdem diese Aufgaben erledigt sind, ruft NtCreateUserProcess die Funktion PspInsertThread auf, um die folgenden Schritte auszuführen: 1. Sofern in einem Attribut ein idealer Prozessor für den Thread angegeben wurde, wird er jetzt initialisiert. 2. Sofern in einem Attribut eine Gruppenaffinität für den Thread angegeben wurde, wird sie jetzt initialisiert. 3. Gehört der Prozess zu einem Job, wird eine Überprüfung durchgeführt, um sicherzustellen, dass die Gruppenaffinität des Threads die Einschränkungen des Jobs nicht verletzt (siehe die Beschreibung weiter vorn). 4. Es wird geprüft, ob der Prozess oder der Thread bereits beendet wurde und ob der Thread schon lauffähig war. Wenn irgendetwas davon zutrifft, schlägt die Threaderstellung fehl. 5. Wenn der Thread zu einem sicheren Prozess (IUM) gehört, wird ein sicheres Threadobjekt erstellt und initialisiert. 6. Der KTHREAD-Teil des Threadobjekts wird durch einen Aufruf von KeStartThread initialisiert. Dazu werden die Schedulereinstellungen vom Besitzerprozess geerbt, der ideale Knoten und Prozessor festgelegt, die Gruppenaffinität aktualisiert, die Basis- und die dynamische Priorität gesetzt (durch Kopieren vom Prozess), das Threadquantum bestimmt und der Thread in die von KPROCESS gepflegte Prozessliste eingefügt (wobei es sich um eine andere Liste als die in EPROCESS handelt). Der Ablauf von CreateProcess Kapitel 3 161
7. Ist der Prozess »tiefgefroren« (dürfen in ihm also keine Threads ausgeführt werden, auch keine neuen), so wird auch der Thread eingefroren. 8. Wenn es sich auf einem Nicht-x86-System um den ersten Thread eines Prozesses handelt (und dieser wiederum nicht der Leerlaufprozess ist), wird der Prozess in eine weitere systemweite Prozessliste eingefügt, die von der globalen Variablen PiProcessListHead gepflegt wird. 9. Der Threadzähler des Prozessobjekts wird erhöht und die E/A- und Seitenpriorität des Besitzerprozesses werden geerbt. Ist die neue Threadanzahl die höchste, die es in dem Prozess bisher gegeben hat, wird auch der Threadspitzenzähler aktualisiert. Bei dem zweiten Thread eines Prozesses wird das Primärtoken eingefroren, sodass es nicht mehr geändert werden kann. 10. Der Thread wird in die Threadliste des Prozesses eingefügt und angehalten, wenn der Erstellerprozess dies verlangt hat. 11. Das Threadobjekt wird in die Prozesshandletabelle eingefügt. 12. Handelt es sich um den ersten Thread des Prozesses (wurde er also im Rahmen eines CreateProcess*-Aufrufs erstellt), werden alle registrierten Callbacks für die Prozesserstellung und danach alle registrierten Callbacks für die Threaderstellung aufgerufen. Wenn irgendeiner dieser Callbacks ein Veto gegen den Erstellungsvorgang einlegt, schlägt er fehl, woraufhin ein entsprechender Status an den Aufrufer zurückgegeben wird. 13. Wurde über ein Attribut eine Jobliste angegeben und ist dies der erste Thread in dem Prozess, so wird der Prozess allen Jobs auf der Liste zugewiesen. 14. Der Thread wird durch einen Aufruf von KeReadyThread zur Ausführung bereit gemacht und tritt in den verzögerten Bereitschaftsstatus ein (siehe Kapitel 4). Phase 5: Spezifische Prozessinitialisierung für das WindowsTeilsystem durchführen Wenn NtCreateUserProcess einen Erfolgscode zurückgibt, sind die erforderlichen Exekutivprozess- und Exekutivthreadobjekte erstellt worden. Anschließend führt CreateProcessInternalW einige spezifische Operationen für das Windows-Teilsystem aus, um die Prozessinitialisierung abzuschließen. 1. Es wird geprüft, ob Windows die Ausführung der ausführbaren Datei erlauben soll. Dazu wird die Abbildversion im Header validiert und nachgeforscht, ob die Windows-Anwendungszertifizierung den Prozess durch eine Gruppenrichtlinie blockiert hat. Bei besonderen Ausgaben von Windows Server 2012 R2, z. B. Windows Storage Server 2012 R2, erfolgen noch weitere Prüfungen, um zu sehen, ob die Anwendung unzulässige APIs importiert. 2. Wenn es von Softwareeinschränkungsrichtlinien vorgeschrieben ist, wird für den neuen Prozess ein eingeschränktes Token erstellt. Anschließend wird die Anwendungskompatibilitätsdatenbank danach abgefragt, ob es für den Prozess einen Eintrag in der Registrierung oder der Anwendungsdatenbank des Systems gibt. Zu diesem Zeitpunkt werden noch 162 Kapitel 3 Prozesse und Jobs
keine Kompatibilitätsshims angewendet. Die Informationen werden im PEB gespeichert, sobald der ursprüngliche Start mit der Ausführung beginnt (Phase 6). 3. CreateProcessInternalW ruft (für nicht geschützte Prozesse) einige interne Funktionen auf, um SxS-Informationen wie Manifestdateien und DLL-Umleitungspfade (siehe den Abschnitt »DLL-Namensauflösung und -Umleitung« weiter hinten in diesem Kapitel) sowie weitere Informationen zu gewinnen, z. B. ob sich die EXE-Datei auf einem Wechselmedium befindet und welche Installererkennungsflags gesetzt sind. Für immersive Prozesse werden auch Versionsinformationen und die Angabe der Zielplattform aus dem Paketmanifest zurückgegeben. 4. Aus den folgenden gesammelten Informationen wird eine Nachricht an das Windows-Teilsystem (Csrss) zusammengestellt: • Name des Pfades und des SxS-Pfades • Prozess- und Threadhandles • Abschnittshandle • Zugriffstokenhandle • Medieninformationen • AppCompat- und Shim-Daten • Informationen über immersive Prozesse • PEB-Adresse • Verschiedene Flags, z. B. zur Angabe, ob der Prozess geschützt ist oder mit erhöhten Rechten ausgeführt werden soll • Ein Flag zur Angabe, ob der Prozess zu einer Windows-Anwendung gehört (damit Csrss bestimmen kann, ob der Startcursor angezeigt werden soll) • Sprachinformationen für die Benutzeroberfläche • DLL-Umleitungs- und .local-Flags (siehe den Abschnitt »Der Abbildlader« weiter hinten in diesem Kapitel) • Informationen aus der Manifestdatei Wenn das Windows-Teilsystem diese Nachricht erhält, führt es die folgenden Schritte aus: 1. CsrCreateProcess dupliziert ein Handle für den Prozess und den Thread. In diesem Schritt wird der Verwendungszähler des Prozesses und des Threads von dem bei der Erstellung festgelegten Wert 1 auf 2 hochgesetzt. 2. Die CSR_PROCESS-Struktur wird zugewiesen. 3. Der Ausnahmeport des neuen Prozesses wird auf den allgemeinen Funktionsport für das Windows-Teilsystem gesetzt, sodass das Teilsystem eine Nachricht empfängt, wenn in dem Prozess eine Zweite-Chance-Ausnahme auftritt (zur Ausnahmebehandlung siehe Kapitel 8 in Band 2). 4. Wird eine neue Prozessgruppe mit dem neuen Prozess als Stammprozess erstellt (Flag ­CREATE_NEW_PROCESS_GROUP in CreateProcess), dann wird sie in CSR_PROCESS eingerichtet. Prozessgruppen sind praktisch, um Steuerereignisse an mehrere Prozesse zu senden, die Der Ablauf von CreateProcess Kapitel 3 163
sich eine Konsole teilen. Weitere Informationen finden Sie in der Dokumentation des Windows SDK über CreateProcess und GenerateConsoleCtrlEvent. 5. Die CSR_THREAD-Struktur wird zugewiesen und initialisiert. 6. CsrCreateThread fügt den Thread in die Threadliste für den Prozess ein. 7. Der Prozesszähler der Sitzung wird heraufgesetzt. 8. Die Prozessbeendigungsebene wird auf den Standardwert 0x280 (siehe SetProcessShut​ downParameters in der Dokumentation des Windows SDK). 9. Die neue CSR_PROCESS-Struktur wird in die Liste der Prozesse für das gesamte Windows-Teilsystem eingefügt. Nachdem Csrss diese Schritte durchgeführt hat, prüft CreateProcessInternalW, ob der Prozess mit erhöhten Rechten ausgeführt wurde (was bedeutet, dass er über ShellExecute ausgeführt wurde und vom AppInfo-Dienst höhere Rechte erhalten hat, nachdem dem Benutzer das Bestätigungsdialogfeld angezeigt wurde). Dazu wird unter anderem nachgesehen, ob es sich bei dem Prozess um ein Setupprogramm handelt. Wenn ja, wird das Prozesstoken geöffnet und das Virtualisierungsflag eingeschaltet, damit die Anwendung virtualisiert wird (siehe die Informationen zur Benutzerkontensteuerung und Virtualisierung in Kapitel 7). Enthält die Anwendung Shims zur Rechteerhöhung oder hat sie in ihrem Manifest eine höhere Rechte­ ebene angefordert, wird der Prozess zerstört und eine Anforderung zur Rechteerhöhung an den AppInfo-Dienst gesendet. Für geschützte Prozesse entfallen die meisten dieser Überprüfungen. Da diese Prozesse für Windows Vista oder höher entworfen sein müssen, besteht keinerlei Grund für eine Überprüfung auf Rechteerhöhung, Virtualisierung und Anwendungskompatibilität. Außerdem könnte es zu Sicherheitslücken führen, wenn Mechanismen wie die Shim-Engine ihre üblichen Hookund Speicherpatch-Techniken bei geschützten Prozessen anwenden dürften, denn dann wäre es Angreifern möglich, eigene Shims einzufügen, die das Verhalten der geschützten Prozesse ändern. Da die Shim-Engine vom Elternprozess installiert wird, der möglicherweise keinen Zugriff auf seinen geschützten Kindprozess hat, würde selbst eine legitime Verwendung von Shims unter Umständen nicht funktionieren. Phase 6: Ausführung des ursprünglichen Threads starten Jetzt ist die Prozessumgebung festgelegt, die Ressourcen für den Thread sind zugewiesen, der Prozess hat einen Thread und das Windows-Teilsystem weiß über den neuen Prozess Bescheid. Sofern der Aufrufer nicht das Flag CREATE_SUSPENDED angegeben hat, wird der ursprüngliche Thread jetzt wieder aufgenommen, sodass er mit der Ausführung beginnen und die restlichen Arbeiten zur Prozessinitialisierung ausführen kann, die im Kontext des neuen Prozesses nötig sind (Phase 7). 164 Kapitel 3 Prozesse und Jobs
Phase 7: Prozessinitialisierung im Kontext des neuen Prozesses durchführen Als Erstes führt der neue Thread die Kernelmodus-Threadstartroutine KiStartUserThread aus. Sie senkt die IRQL-Ebene des Threads von DPC (Deferred Procedure Call, verzögerter Prozeduraufruf) in APC und ruft dann die ursprüngliche Threadroutine des Systems auf, PspUserThreadStartup. Dieser Routine wird die vom Benutzer angegebene Threadstartadresse als Parameter übergeben. PspUserThreadStartup führt dann die folgenden Aktionen aus: 1. Auf einer x86-Architektur installiert sie eine Ausnahmekette. (Auf anderen Architekturen weicht die Vorgehensweise ab; siehe Kapitel 8 in Band 2.) 2. Sie senkt die IRQL-Ebene auf PASSIVE_LEVEL (Ebene 0, was die einzige IRQL-Ebene ist, auf der Benutzercode ausgeführt werden darf). 3. Sie deaktiviert die Möglichkeit, das primäre Prozesstoken zur Laufzeit auszutauschen. 4. Wurde der Thread beim Start aus irgendeinem Grund abgebrochen, so wird er beendet. Es werden dann keine weiteren Maßnahmen unternommen. 5. Sie legt aufgrund der Informationen in den Kernelmodus-Datenstrukturen die Gebietsschema-ID und den idealen Prozessor im TEB fest und prüft, ob die Threaderstellung fehlgeschlagen ist. 6. Sie ruft DbgkCreateThread auf, die prüft, ob Abbildbenachrichtigungen für den neuen Prozess gesendet wurden. Wenn das nicht der Fall ist, Benachrichtigungen aber aktiviert sind, wird eine Abbildbenachrichtigung erst für den Prozess und dann für den Abbildladevorgang von Ntdll.dll gesendet. Dieser Vorgang erfolgt in dieser Phase und nicht bei der ursprünglichen Zuordnung der Abbilder, da die für Kernel-Callouts notwendige Prozess-ID dann noch gar nicht zugewiesen ist. 7. Danach folgt eine weitere Überprüfung, um zu sehen, ob es sich um einen zu debuggenden Prozess handelt. Wenn das der Fall ist, aber noch keine Debuggerbenachrichtigungen gesendet wurden, wird durch das Debugobjekt (falls vorhanden) eine Prozesserstellungsnachricht gesendet, sodass das Prozessstart-Debugereignis (CREATE_PROCESS_DEBUG_INFO) an den entsprechenden Debuggerprozess gesendet werden kann. Darauf folgt ein ähnliches Threadstart-Debugereignis sowie ein weiteres Debugereignis für den Abbildladevorgang von Ntdll.dll. Anschließend wartet DbgkCreateThread auf eine Antwort des Debuggers (über die Funktion ContinueDebugEvent). 8. Sie prüft, ob das Anwendungs-Prefetching auf dem System aktiviert ist. Wenn ja, ruft sie den Prefetcher (und SuperFetch) auf, um die Prefetch-Anweisungsdatei (falls vorhanden) und die während der ersten zehn Sekunden der letzten Prozessausführung referenzierten Prefetchseiten zu verarbeiten. (Mehr über den Prefetcher und SuperFetch erfahren Sie in Kapitel 5.) Der Ablauf von CreateProcess Kapitel 3 165
9. Sie prüft, ob das systemweite Cookie in der Struktur SharedUserData eingerichtet wurde. Wenn nicht, generiert sie es auf der Grundlage eines Hashs von Systeminformationen wie der Anzahl der verarbeiteten Interrupts, der DPC-Auslieferungen, der Seitenfehler, der Interruptzeit und einer Zufallszahl. Dieses systemweite Cookie wird als Schutz gegen bestimmte Arten von Exploits bei der internen Codierung und Decodierung von Zeigern verwendet, u. a. im Heap-Manager. (Mehr über die Sicherheit des Heap-Managers erfahren Sie in Kapitel 5.) 10. Bei einem sicheren Prozess (IUM-Prozess) wird HvlStartSecureThread aufgerufen, um die Steuerung für den Start der Threadausführung an den sicheren Kernel zu übertragen. Diese Funktion gibt die Steuerung nur zurück, wenn der Thread vorhanden ist. 11. Sie richtet den ursprünglichen Thunkkontext zur Ausführung der Initialisierungsroutine des Abbildladers (LdrInitializeThunk in Ntdll.dll) und den systemweiten Threadstartstub ein (RtlUserThreadStart in Ntdll.dll). Dazu wird der Kontext des Threads direkt bearbeitet und dann eine Beendigung des Systemdienstbetriebs verlangt, wodurch dieser spezielle Benutzerkontext geladen wird. Die Routine LdrInitializeThunk initialisiert den Lader, den Heap-Manager, die NLS-Tabellen, die Arrays für den lokalen Threadspeicher (TLS) und den lokalen Fiberspeicher (FLS) sowie kritische Abschnittsstrukturen. Anschließend lädt sie alle erforderlichen DLLs und ruft die DLL-Eintrittspunkte mit dem Funktionscode von DLL_PROCESS_ATTACH auf. Sobald die Funktion die Steuerung zurückgibt, stellt NtContinue den neuen Benutzerkontext wieder her und kehrt in den Benutzermodus zurück. Jetzt beginnt die echte Threadausführung. RtlUserThreadStart verwendet die Adresse des eigentlichen Abbildeintrittspunktes und den Startparameter und ruft den Eintrittspunkt der Anwendung auf. Diese beiden Parameter wurden bereits vom Kernel auf den Stack gelegt. Für diesen komplizierten Ablauf gibt es zwei Gründe: QQ Der Abbildlader in Ntdll.dll wird dadurch in die Lage versetzt, den Prozess intern und hinter den Kulissen einzurichten, sodass anderer Benutzermoduscode ordnungsgemäß ausgeführt werden kann. (Andernfalls gäbe es keinen Heap, keinen lokalen Threadspeicher usw.) QQ Wenn alle Threads in einer gemeinsamen Routine beginnen, können sie mit einer Ausnahmebehandlung versehen werden. Dadurch kann Ntdll.dll es erkennen, wenn sie abstürzen, und den Filter für nicht behandelte Ausnahmen in Kernel32.dll aufrufen. Außerdem kann Ntdll.dll die Threadbeendigung beim Rücksprung von der Startroutine des Threads koordinieren und verschiedene Aufräumarbeiten durchführen. Anwendungsentwickler können SetUnhandledExceptionFilter aufrufen, um ihren eigenen Code für den Umgang mit nicht behandelten Ausnahmen hinzuzufügen. 166 Kapitel 3 Prozesse und Jobs
Experiment: Den Prozessstart verfolgen Sie haben jetzt in allen Einzelheiten gesehen, wie ein Prozess startet und welche verschiedenen Operationen erforderlich sind, um mit der Ausführung einer Anwendung zu beginnen. Nun wollen wir uns mit Process Monitor einige der Dateieingaben und -ausgaben ansehen, die bei diesen Vorgängen erfolgen, sowie einige der Registrierungsschlüssel, auf die dabei zugegriffen wird. Dieses Experiment vermittelt zwar keine Gesamtansicht aller internen Schritte, die wir zuvor beschrieben haben, doch Sie können dabei verschiedene Teile des Systems in Aktion erleben, vor allem den Prefetcher und SuperFetch, die Überprüfung der Optionen zur Ausführung der Abbilddatei, weitere Kompatibilitätsprüfungen sowie die Zuordnung der DLL des Abbildladers. Als Beispiel verwenden wir eine sehr einfache ausführbare Datei, nämlich Notepad.exe (den Windows-Editor), die wir an einer Eingabeaufforderung (Cmd.exe) starten. Beachten Sie, dass wir uns dabei sowohl die Operationen innerhalb von Cmd.exe als auch die in Notepad.exe ansehen. Wie Sie bereits wissen, wird ein Großteil der Arbeiten im Benutzermodus von der Funktion CreateProcessInternalW erledigt, die von dem Elternprozess aufgerufen wird, bevor der Kernel das neue Prozessobjekt erstellt hat. Gehen Sie zur Vorbereitung wie folgt vor: 1. Fügen Sie im Process Monitor zwei Filter hinzu, einen für Cmd.exe und einen für Notepad.exe. Dies sind die beiden einzigen Prozesse, die Sie einschließen sollten. Sorgen Sie dafür, dass zurzeit keine Instanzen dieser beiden Prozesse laufen, damit sichergestellt ist, dass Sie sich auch die richtigen Ereignisse ansehen. Das Filterfenster sollte wie folgt aussehen: 2. Vergewissern Sie sich, dass die Ereignisprotokollierung zurzeit deaktiviert ist (öffnen Sie File und deaktivieren Sie Capture Events). Starten Sie dann die Eingabeaufforderung. 3. Aktivieren Sie die Ereignisprotokollierung (öffnen Sie File und wählen Sie Event Logging, drücken Sie (Strg) + (E) oder klicken Sie auf das Lupensymbol auf der Symbolleiste). Geben Sie dann Notepad.exe ein und drücken Sie die Eingabetaste. Auf einem typischen Windows-System erscheinen nun etwa 500 bis 3500 Ereignisse. → Der Ablauf von CreateProcess Kapitel 3 167
4. Stoppen Sie die Erfassung und blenden Sie die Spalten Sequence und Time of Day aus, sodass Sie sich besser auf die für unser Experiment interessanten Spalten konzentrieren können. Das Fenster sollte jetzt so ähnlich wie in dem folgenden Screenshot aussehen. Wie in Phase 1 des Ablaufs von CreateProcess beschrieben, liest Cmd.exe, kurz bevor der Prozess gestartet und der erste Thread erstellt wird, in der Registrierung, und zwar in HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ Notepad.exe. Da mit Notepad.exe keine Optionen zur Ausführung des Abbilds verbunden sind, wird der Prozess regulär erstellt. Wie bei anderen Ereignissen im Protokoll von Process Monitor können Sie auch hier durch Untersuchung des Ereignisstacks erkennen, welche Teile des Ablaufs zur Prozesserstellung im Benutzer- und welche im Kernelmodus durchgeführt wurden und durch welche Routinen das geschah. Dazu doppelklicken Sie auf das Ereignis RegOpenKey und wechseln zur Registerkarte Stack. Der folgende Screenshot zeigt den Standardstack auf einem 64-Bit-Computer mit Windows 10. → 168 Kapitel 3 Prozesse und Jobs
Der Stack zeigt, dass Sie bereits den Teil der Prozesserstellung erreicht haben, der im Kernelmodus ausgeführt wird (durch NtCreateUserProcess) und dass die Hilfsroutine PspAllocateProcess für die vorliegende Überprüfung verantwortlich ist. Wenn Sie sich die Liste der Ereignisse nach dem Erstellen des Threads und des Prozesses ansehen, werden Ihnen drei Gruppen von Ereignissen auffallen: QQ Eine einfache Überprüfung von Flags zur Anwendungskompatibilität, durch die der Prozesserstellungscode des Benutzermodus erkennen kann, ob eine Überprüfung in der Anwendungskompatibilitätsdatenbank durch die Shim-Engine erforderlich ist. QQ Mehrere Lesevorgänge in den SxS- (Side-By-Side), Manifest- und MUI-/Sprachschlüsseln, die zu dem zuvor erwähnten Assemblyframework gehören. QQ Datei-E/A für eine oder mehrere .sdb-Dateien, bei denen es sich um die Anwendungskompatibilitätsdatenbanken auf dem System handelt. Bei diesen Ein-/Ausgaben finden zusätzliche Überprüfungen statt, um herauszufinden, ob für die Shim-Engine diese Anwendung aufgerufen werden muss. Da der Editor ein gutwilliges Microsoft-Programm ist, sind hier keine Shims nötig. → Der Ablauf von CreateProcess Kapitel 3 169
Der folgende Screenshot zeigt die nächsten Ereignisse, die innerhalb des Notepad-Prozesses selbst auftreten. Dies sind die Aktionen, die der Benutzermodus-Threadstartwrapper im Kernelmodus ausgelöst hat. Die ersten beiden sind Debugbenachrichtigungen über das Laden von Abbildern von Notepad.exe und Ntdll.dll. Diese Nachrichten können erst jetzt generiert werden, da der Code im Prozesskontext des Editors läuft und nicht mehr im Kontext der Eingabeaufforderung. Als Nächstes greift der Prefetcher ein und sucht nach einer Prefetch-Datenbankdatei, die bereits für den Editor generiert wurde. (Weitere Informationen über den Prefetcher erhalten Sie in Kapitel 5.) Wurde der Editor auf dem System schon mindestens einmal ausgeführt, so ist diese Datenbank vorhanden, sodass der Prefetcher nun beginnt, die darin angegebenen Befehle auszuführen. In diesem Fall sehen Sie, wenn Sie weiter nach unten scrollen, wie verschiedene DLLs gelesen und abgefragt werden. Anders als beim gewöhnlichen Laden von DLLs – durch den Abbildlader des Benutzermodus, der sich die Importtabellen ansieht, oder manuell durch Anwendungen – werden die Ereignisse hier vom Prefetcher hervorgerufen, der die vom Editor benötigten Bibliotheken kennt. Das gewöhnliche Laden von DLLs folgt jedoch als Nächstes, und dabei werden Sie ähnliche Ereignisse wie hier sehen. → 170 Kapitel 3 Prozesse und Jobs
Die Ereignisse werden jetzt von Benutzermoduscode generiert, der aufgerufen wurde, als die Kernelmodus-Wrapperfunktion ihre Arbeit beendet hatte. Daher stammen die ersten Ereignisse von LdrpInitializeProcess, die von LdrInitializeThunk für den ersten Thread des Prozesses aufgerufen wurde. Das können Sie bestätigen, indem Sie sich den Ereignisstack ansehen – beispielsweise das Abbildladeereignis für Kernel32.dll, das im folgenden Screenshot zu sehen ist. → Der Ablauf von CreateProcess Kapitel 3 171
Es folgen weitere Ereignisse, die von dieser Routine und ihren Hilfsfunktionen hervorgerufen wurden. Schließlich gelangen Sie zu den Ereignissen der Funktion WinMain des Editors, in der der Code ausgeführt wird, der der Kontrolle des Entwicklers unterliegt. Die ausführliche Beschreibung aller Ereignisse und Benutzermoduskomponenten, die bei der Prozessausführung auftreten, würde ein ganzes Kapitel füllen. Die Untersuchung weiterer Ereignisse überlassen wir Ihnen daher als Übungsaufgabe. Einen Prozess beenden Ein Prozess ist ein Container und eine Grenze. Das bedeutet, dass Ressourcen, die von einem Prozess verwendet werden, in anderen Prozessen nicht unbedingt sichtbar sind. Um Informa­ tionen zwischen Prozessen auszutauschen, ist daher ein Kommunikationsmechanismus erforderlich. Es ist auch nicht möglich, dass ein Prozess versehentlich willkürliche Bytes im Arbeitsspeicher eines anderen Prozesses überschreibt, denn das würde den ausdrücklichen Aufruf einer Funktion wie WriteProcessMemory erfordern. Dazu wiederum müsste ausdrücklich ein Handle mit der korrekten Zugriffsmaske (PROCESS_VM_WRITE) geöffnet werden, wobei nicht einmal sichergestellt ist, dass dieser Zugriff auch gewährt wird. Diese natürliche Isolation der Prozesse bedeutet auch, dass eine Ausnahme in einem Prozess keine Auswirkungen auf andere Prozesse hat. Schlimmstenfalls stürzt der Prozess ab, aber der Rest des Systems bleibt intakt. Ein Prozess kann durch den Aufruf der Funktion ExitProcess ordnungsgemäß beendet werden. Für viele Prozesse – je nach Linkereinstellung – ruft der Prozessstartcode für den ersten Thread ExitProcess im Namen des Prozesses auf, wenn der Thread von der Hauptfunktion zurückspringt. Ordnungsgemäß bedeutet hier, dass die in den Prozess geladenen DLLs über die anstehende Beendigung des Prozesses benachrichtigt werden, damit sie die Gele- 172 Kapitel 3 Prozesse und Jobs
genheit bekommen, noch etwas zu tun. Diese Benachrichtigung erfolgt durch den Aufruf ihrer D­ llMain-Funktion mit DLL_PROCESS_DETACH. ExitProcess kann nur von dem zu beendenden Prozess selbst aufgerufen werden. Eine nicht ordnungsgemäße Beendigung eines Prozesses ist mit der Funktion TerminateProcess möglich, die von außerhalb des Prozesses aufgerufen werden kann. (Process Explorer und der Task-Manager verwenden diese Funktion, wenn sie dazu aufgefordert werden.) TerminateProcess benötigt ein Handle für den Prozess, der mit der Zugriffsmaske PROCESS_TERMINATE geöffnet wird. Allerdings wird dieser Zugriff nicht zwangsläufig gewährt. Daher ist es nicht einfach und manchmal sogar unmöglich, bestimmte Prozesse zu beenden (z. B. Csrss). Nicht ordnungsgemäß bedeutet hier, dass die DLLs keine Chance mehr haben, irgendwelchen Code auszuführen (DLL_PROCESS_DETACH wird nicht gesendet), und alle Threads abrupt abgebrochen werden. Das kann zu Datenverlusten führen, etwa wenn ein Dateicache keine Möglichkeit mehr hat, seine Daten in die Zieldatei zu übertragen. Wie auch immer ein Prozess beendet wird, es kann keine Lecks geben. Der gesamte private Arbeitsspeicher des Prozesses wird automatisch vom Kernel freigegeben, der Adressraum wird zerstört, alle Handles zu Kernelobjekten werden geschlossen usw. Solange es noch offene Handles zu dem Prozess gibt (weil die EPROCESS-Struktur nach wie vor existiert), können andere Prozesse Zugriff auf einige der Prozessverwaltungsinformationen bekommen, z. B. auf den Prozessbeendigungscode (GetExitCodeProcess). Nach dem Schließen dieser Handles wird die EPROCESS-Struktur jedoch zerstört, sodass wirklich nichts mehr von dem Prozess übrig ist. Allerdings können Drittanbietertreiber im Namen des Prozesses Zuweisungen im Kernelspeicher vornehmen, etwa aufgrund einer IOCTL (siehe Kapitel 6) oder einfach einer Prozessbenachrichtigung. Es liegt dann in ihrer Verantwortung, diesen Poolarbeitsspeicher freizugeben. Windows hält sich nicht über den Kernelspeicher auf dem Laufenden, den ein Prozess besitzt, und räumt ihn auch nicht auf (abgesehen von Arbeitsspeicher, der aufgrund vom Prozess erstellter Handles mit Objekten belegt ist). Damit der Treiber seinen Freigabepflichten nachkommen kann, wird er gewöhnlich über eine IRP_MJ_CLOSE-, IRP_MJ_CLEANUP- oder Prozessbeendigungsnachricht darüber informiert, dass das Handle für das Geräteobjekt geschlossen wurde. Der Abbildlader Wenn auf dem System ein Prozess gestartet wird, erstellt der Kernel ein Prozessobjekt dafür und führt verschiedene Initialisierungstätigkeiten aus. Dies führt jedoch nicht zur Ausführung der Anwendung, sondern bereitet lediglich den Kontext und die Umgebung vor. Im Gegensatz zu Treibern, bei denen es sich um Kernelmoduscode handelt, werden Anwendungen im Benutzermodus ausgeführt. Der Großteil der eigentlichen Initialisierungsarbeit erfolgt außerhalb des Kernels. Sie wird durch den Abbildlader erledigt, der intern als Ldr bezeichnet wird. Der Abbildlader befindet sich in der Benutzermodus-System-DLL Ntdll.dll und nicht in der Kernelbibliothek. Daher verhält er sich wie Standardcode aus einer DLL und unterliegt denselben Einschränkungen für den Speicherzugriff und die Sicherheitsrechte. Das Besondere an diesem Code ist, dass er im laufenden Prozess garantiert vorhanden ist (da Ntdll.dll stets geladen wird) und als erster Benutzermoduscode eines neuen Prozesses ausgeführt wird. Der Abbildlader Kapitel 3 173
Da der Lader vor dem eigentlichen Anwendungscode ausgeführt wird, ist er für Benutzer und Entwickler gewöhnlich nicht sichtbar. Obwohl seine Initialisierungsaufgaben verborgen sind, kann ein Programm zur Laufzeit mit den Schnittstellen des Laders kommunizieren, z. B. beim Laden oder Entladen einer DLL oder beim Abfragen der Basisadresse einer DLL. Der Lader ist unter anderem für die folgenden Aufgaben zuständig: QQ Initialisierung des Benutzermodusstatus der Anwendung, z. B. durch Erstellen des ursprünglichen Heaps und Einrichten der Slots für den lokalen Thread- und Fiberspeicher (TLS bzw. FLS). QQ Analyse der Importtabelle (IAT) der Anwendung, um nach allen erforderlichen DLLs zu suchen (mit anschließender rekursiver Analyse der IATs der einzelnen DLLs), gefolgt von der Analyse der Exporttabellen der DLLs, um sicherzustellen, dass die Funktion tatsächlich vorhanden ist. (Mit besonderen Forwarder-Einträgen kann ein Export zu einer anderen DLL umgeleitet werden.) QQ Laden und Entladen von DLLs zur Laufzeit und bei Bedarf und Pflege einer Liste aller geladenen Module (Moduldatenbank). QQ Handhabung von Manifestdateien für die SxS-Unterstützung (Side-by-Side) sowie von MUI-Dateien und -Ressourcen (Multiple Language User Interface). QQ Durchsuchen der Anwendungskompatibilitätsdatenbank nach Shims und ggf. Laden der DLL für die Shim-Engine. QQ Aktivieren der Unterstützung für API-Sets und API-Umleitung, einem Kernbestandteil der One Core-Funktionalität, die es ermöglicht, UWP-Anwendungen zu erstellen (Universal Windows Platform). QQ Aktivieren von Vorbeugungsmaßnahmen zur dynamischen Laufzeitkompatibilität durch SwitchBack-Mechanismen sowie Interaktion mit der Shim-Engine und den Anwendungsverifizierungsmechanismen. Die meisten dieser Aufgaben sind unverzichtbar, um es der Anwendung zu ermöglichen, ihren eigenen Code auszuführen. Ohne sie würden alle Vorgänge vom Aufruf externer Funktionen bis zur Verwendung des Heaps sofort fehlschlagen. Nachdem der Prozess erstellt ist, ruft der Lader die native API NtContinue auf, um die Ausführung gemäß dem Ausnahmeframe fortzusetzen, der auf dem Stack liegt, wie es auch jeder andere Ausnahmehandler tun würde. Dieser Ausnahmeframe wurde vom Kernel erstellt und enthält den eigentlichen Eintrittspunkt der Anwendung. Da der Lader keinen Standardaufruf ausführt oder in die laufende Anwendung hineinspringt, sind die Initialisierungsfunktionen des Laders im Aufrufbaum der Stackablaufverfolgung für einen Thread niemals zu sehen. 174 Kapitel 3 Prozesse und Jobs
Experiment: Den Abbildlader beobachten In diesem Experiment verwenden Sie globale Flags, um die Debuggerfunktion der Lader­ snapshots zu aktivieren. Damit können Sie beim Debuggen des Anwendungsstarts die Debugausgabe des Abbildladers einsehen. 1. Starten Sie in dem Verzeichnis, in dem Sie WinDbg installiert haben, die Anwendung Gflags.exe und klicken Sie auf die Registerkarte Image File. 2. Geben Sie Notepad.exe in das Feld Image ein und drücken Sie (Tab). Dadurch werden die verschiedenen Optionen aktiviert. Wählen Sie Show Loader Snaps aus und klicken Sie auf OK oder Apply. 3. Starten Sie WinDbg, öffnen Sie das Menü File, wählen Sie Open Executable und suchen Sie C:\Windows\System32\Notepad.exe, um sie zu starten. Es werden jetzt Debuginformationen wie die folgenden angezeigt: 0f64:2090 @ 02405218 - LdrpInitializeProcess - INFO: Beginning execution of notepad.exe (C:\WINDOWS\notepad.exe) Current directory: C:\Program Files (x86)\Window Kits\10\Debuggers\ Package directories: (null) 0f64:2090 @ 02405218 - LdrLoadDll - ENTER: DLL name: KERNEL32.DLL 0f64:2090 @ 02405218 - LdrpLoadDllInternal - ENTER: DLL name: KERNEL32.DLL 0f64:2090 @ 02405218 - LdrpFindKnownDll - ENTER: DLL name: KERNEL32.DLL 0f64:2090 @ 02405218 - LdrpFindKnownDll - RETURN: Status: 0x00000000 0f64:2090 @ 02405218 - LdrpMinimalMapModule - ENTER: DLL name: C:\WINDOWS\ System32\KERNEL32.DLL ModLoad: 00007fff'5b4b0000 00007fff'5b55d000 C:\WINDOWS\System32\KERNEL32. DLL 0f64:2090 @ 02405218 - LdrpMinimalMapModule - RETURN: Status: 0x00000000 0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-wincorertlsupportl1-2-0.dll was redirected to C:\WINDOWS\SYSTEM32\ntdll.dll by API set 0f64:2090 @ 02405218 - LdrpFindKnownDll - ENTER: DLL name: KERNELBASE.dll 0f64:2090 @ 02405218 - LdrpFindKnownDll - RETURN: Status: 0x00000000 0f64:2090 @ 02405218 - LdrpMinimalMapModule - ENTER: DLL name: C:\WINDOWS\ System32\KERNELBASE.dll ModLoad: 00007fff'58b90000 00007fff'58dc6000 C:\WINDOWS\System32\KERNELBASE. dll 0f64:2090 @ 02405218 - LdrpMinimalMapModule - RETURN: Status: 0x00000000 0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-wineventingprovider-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ kernelbase.dll by API set 0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-wincoreapiqueryl1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ntdll.dll by API set → Der Abbildlader Kapitel 3 175
4. Der Debugger hält schließlich mitten im Ladercode an. Dies geschieht an der Stelle, an der der Imagelader prüft, ob ein Debugger angeschlossen ist, und einen Haltepunkt auslöst. Wenn Sie (g) drücken, um die Ausführung fortzusetzen, werden weitere Meldungen des Laders angezeigt. Schließlich erscheint der Windows-Editor. 5. Arbeiten Sie mit dem Editor. Dabei können Sie beobachten, dass bei bestimmten Operationen der Lader aufgerufen wird, z. B. wenn Sie das Dialogfeld Speichern oder Öffnen aufrufen. Das zeigt, dass der Lader nicht nur beim Start ausgeführt wird, sondern weiterhin auf Threadanforderungen reagiert, die das verzögerte Laden weiterer Module verursachen können (die dann nach Gebrauch wieder entladen werden). Frühe Prozessinitialisierung Der Lader befindet sich in Ntdll.dll, also einer nativen DLL, die nicht mit einem bestimmten Teilsystem verbunden ist. Daher unterliegen alle Prozesse demselben Laderverhalten (mit einigen kleinen Unterschieden). Wir haben uns bereits ausführlich die Schritte angesehen, die zum Erstellen eines Prozesses im Kernelmodus führen, und einige der Aufgaben beschrieben, die die Windows-Funktion CreateProcess erledigt. Jetzt wollen wir uns mit den weiteren Arbeiten beschäftigen, die unabhängig vom Teilsystem im Benutzermodus stattfinden, sobald die Ausführung der ersten Benutzermodusanweisung beginnt. Beim Start eines Prozesses führt der Lader die folgenden Schritte aus: 1. Er prüft, ob LdrpProcessInitialized bereits auf 1 eingestellt oder das Flag SkipLoaderInit im TEB gesetzt wurde. Wenn ja, überspringt er die gesamte Initialisierung und wartet drei Sekunden lang darauf, dass LdrpProcessInitializationComplete aufgerufen wird. Dies geschieht etwa, wenn die Windows-Fehlerberichterstattung Prozessreflexion einsetzt, oder bei anderen Prozessabspaltungen, bei denen keine Laderinitialisierung benötigt wird. 2. Er setzt LdrInitState auf 0, was bedeutet, dass der Lader nicht initialisiert ist, sowie das PEB-Flag ProcessInitializing und das TEB-Flag RanProcessInit auf 1. 3. Er initialisiert die Ladersperre im PEB. 4. Er initialisiert die dynamische Funktionstabelle für die Abwicklungs- und Ausnahmeunterstützung in JIT-Code. 5. Er initialisiert den veränderlichen, schreibgeschützten Heap-Abschnitt MRDATA (Mutable Read Only), in dem sicherheitsrelevante globale Variablen gespeichert werden, die von Exploits nicht bearbeitet werden dürfen (siehe Kap. 7). 6. Er initialisiert die Laderdatenbank im PEB. 7. Er unterstützt die Sprachunterstützungstabellen (National Language Support, NLS) für den Prozess. 8. Er stellt den Abbildpfadnamen für die Anwendung zusammen. 9. Er erfasst die SEH-Ausnahmehandler aus dem Abschnitt .pdata und erstellt die internen Ausnahmetabellen. 176 Kapitel 3 Prozesse und Jobs
10. Er erfasst die Systemaufrufthunks für die fünf kritischen Laderfunktionen NtCreateSection, NtOpenFile, NtQueryAttributesFile, NtOpenSection und NtMapViewOfSection. 11. Er liest die Vorbeugeoptionen für die Anwendung (die vom Kernel über die exportierte Variable LdrSystemDllInitBlock übergeben wurden). Sie werden ausführlich in Kapitel 7 beschrieben. 12. Er fragt den Registrierungsschlüssel Image File Execution Options für die Anwendung ab. Dazu gehören Optionen wie die globalen Flags (gespeichert in GlobalFlags), Debuggingoptionen für den Heap (DisableHeapLookaside, ShutdownFlags und FrontEndHeapDebugOptions), Ladereinstellungen (UnloadEventTraceDepth, MaxLoaderThreads, UseImpersonatedDeviceMap), ETW-Einstellungen (TracingFlags) sowie weitere Optionen wie MinimumStackCommitInBytes und MaxDeadActivationContexts. Im Rahmen dieser Arbeiten werden auch das Paket Application Verifier und die zugehörigen Verifizierungs-DLLs initialisiert und die ControlFlow​ Guard-Optionen (CFG) von CFGOptions gelesen. 13. Er schaut im Header der ausführbaren Datei nach, ob es sich um eine .NET-Anwendung handelt (was durch das Vorhandensein eines .NET-spezifischen Abbildverzeichnisses angezeigt wird) und ob das Abbild im 32-Bit-Format vorliegt. Außerdem fragt er den Kernel ab, um zu ermitteln, ob der Prozess ein Wow64-Prozess ist. Bei Bedarf kann er auch mit einem reinen Abbildlader-Abbild von 32 Bit umgehen, für das Wow64 nicht benötigt wird. 14. Er lädt die im Ladekonfigurationsverzeichnis der ausführbaren Datei angegebenen Konfigurationsoptionen. Diese Optionen werden vom Entwickler bei der Kompilierung der Anwendung definiert und können vom Compiler und vom Linker genutzt werden, um bestimmte Sicherheits- und Vorbeugemerkmale wie CFG einzurichten. Sie steuern das Verhalten der ausführbaren Datei. 15. Er nimmt eine Minimalinitialisierung des FLS und TLS vor. 16. Er richtet die Debuggingoptionen für kritische Abschnitte ein, erstellt eine Datenbank für die Ablaufverfolgung des Benutzermodusstacks (sofern das entsprechende globale Flag gesetzt war) und ruft StackTraceDatabaseSizeInMb von Image File Execution Options ab. 17. Er initialisiert den Heap-Manager für den Prozess und erstellt den ersten Prozessheap. Um die erforderlichen Parameter einzurichten, zieht dieser Heap verschiedene Optionen für die Ladekonfiguration, die Abbilddateiausführung, die globalen Flags und den Header der ausführbaren Datei heran. 18. Er aktiviert den Prozess Terminate für die Vorbeugemaßnahme gegen Heapbeschädigung, falls diese eingeschaltet ist. 19. Er initialisiert das Ausnahme-Dispatchprotokoll, falls das entsprechende globale Flag gesetzt ist. 20. Er initialisiert das Threadpoolpaket, das die Threadpool-API unterstützt. Es fragt NUMA-­ Informationen ab und berücksichtigt sie. 21. Er initialisiert und konvertiert den Umgebungs- und den Parameterblock, vor allem zur Unterstützung von WoW64-Prozessen. 22. Er öffnet das Objektverzeichnis \KnownDlls und erstellt den Pfad für bekannte DLLs. Für einen WoW64-Prozess wird stattdessen \KnownDlls32 verwendet. Der Abbildlader Kapitel 3 177
23. Für Store-Anwendungen liest er die Optionen der Anwendungsmodellrichtlinie, die in den Claims WIN://PKG und P://SKUID des Tokens codiert sind (siehe den Abschnitt »Anwendungscontainer« in Kapitel 7). 24. Er bestimmt das aktuelle Verzeichnis, den Systempfad und den Standardladepfad (zum Laden von Abbildungen und Öffnen von Dateien) des Prozesses sowie die Regeln für die Standardreihenfolge bei der DLL-Suche. Dazu gehört es, die aktuellen Richtlinieneinstellungen für Universal- (UWP), Desktop-Bridge- (Centennial) bzw. Silverlight-Anwendungen (Windows Phone 8) oder -Dienste zu lesen. 25. Er erstellt den ersten Eintrag in der Laderdatentabelle für Ntdll.dll und fügt ihn in die Moduldatenbank ein. 26. Er erstellt die Tabelle für den Abwicklungsverlauf. 27. Er initialisiert den Parallellader, der mithilfe des Threadpools und gleichzeitiger Threads alle Abhängigkeiten lädt, die keine Kreuzabhängigkeiten aufweisen. 28. Er erstellt den nächsten Eintrag in der Laderdatentabelle für die ausführbare Hauptdatei und fügt ihn in die Moduldatenbank ein. 29. Bei Bedarf verlagert er das Abbild der ausführbaren Hauptdatei. 30. Er initialisiert die Anwendungsverifizierung, falls sie aktiviert ist. 31. Bei einem Wow64-Prozess initialisiert er die Wow64-Engine. Der 64-Bit-Lader schließt dabei seine Initialisierung ab und der 32-Bit-Lader übernimmt die Kontrolle und startet die meisten bis jetzt beschriebenen Operationen neu. 32. Wenn es sich um ein .NET-Abbild handelt, validiert der Lader es. Anschließend lädt er ­Mscoree.dll (ein .NET-Laufzeit-Shim) und ruft den Eintrittspunkt der ausführbaren Hauptdatei ab (_CorExeMain). Damit überschreibt er den Ausnahmedatensatz, sodass dieser Eintrittspunkt statt der regulären main-Funktion verwendet wird. 33. Er initialisiert die TLS-Slots des Prozesses. 34. Für Windows-Teilsystem-Anwendungen lädt er manuell Kernel32.dll und Kernelbase.dll unabhängig von den tatsächlichen Importen des Prozesses. Er verwendet diese Bibliotheken nach Bedarf, um den SRP/Safer-Mechanismus zu initialisieren (Software Restriction Policies) und um die Thunkfunktion für die Initialisierung des Windows-Teilsystemthreads abzufangen. Schließlich löst er jegliche API-Set-Abhängigkeiten zwischen diesen beiden Bibliotheken auf. 35. Er initialisiert die Shim-Engine und parst die Shim-Datenbank. 36. Er aktiviert den parallelen Abbildlader, sofern mit den zuvor gescannten Kernladerfunk­ tionen keine Systemaufruf-Hooks oder »Umleitungen« verbunden sind. Dies geschieht auf der Grundlage der Anzahl von Laderthreads, die durch Richtlinien und Optionen zur Abbildausführung festgelegt wurden. 37. Er setzt die Variable LdrInitState auf 1, was »Importladevorgang läuft« bedeutet. Jetzt ist der Abbildlader bereit, die Importtabelle der ausführbaren Datei für die Anwendung zu analysieren und jegliche DLLs zu laden, die beim Kompilieren der Anwendung dynamisch verlinkt wurden. Das geschieht sowohl bei .NET-Images, deren Importe durch Aufrufe in der 178 Kapitel 3 Prozesse und Jobs
.NET-Laufzeitumgebung verarbeitet werden, als auch für reguläre Abbilder. Da jede importierte DLL wiederum ihre eigene Importtabelle haben kann, wurde diese Operation früher rekursiv durchgeführt, bis alle DLLs berücksichtigt und alle zu importierenden Funktionen gefunden waren. Beim Laden der einzelnen Dateien behielt der Lader Statusinformationen dafür bei und stellte die Moduldatenbank auf. In neueren Windows-Versionen erstellt der Lader stattdessen im Voraus eine Abhängigkeitsstruktur mit Knoten für die einzelnen DLLs und einem Abhängigkeitsgraphen. Dabei kann er Knoten herausarbeiten, die parallel geladen werden können. Wird eine Serialisierung benötigt, wird die Arbeitswarteschlange des Threadpools geleert, was als Synchronisierungspunkt fungiert. Ein solcher Punkt tritt auf dem Aufruf aller DLL-Initialisierungsroutinen für die statischen Importe auf, was einer der letzten Schritte des Laders ist. Nachdem dies erledigt ist, werden die statischen TLS-Initialisierer aufgerufen. Bei Windows-Anwendungen wird zwischen diesen beiden Schritten zu Anfang die Thunkfunktionen für die Initialisierung des Kernel32-Threads (BaseThreadInitThunk) und am Ende die Post-Prozessinitialisierungsroutine von Kernel32 aufgerufen. DLL-Namensauflösung und -Umleitung Bei der Namensauflösung ermittelt das System aus dem Namen einer PE-Binärdatei die physische Datei, wenn der Aufrufer keine eindeutige Dateikennung angegeben hat oder es nicht kann. Da die Speicherorte der verschiedenen Verzeichnisse (Anwendungsverzeichnis, Systemverzeichnis usw.) zur Verlinkungszeit nicht hartcodiert werden können, schließt dies die Auflösung aller Abhängigkeiten von Binärdateien sowie LoadLibrary-Operationen ein, in denen der Aufrufer keinen vollständigen Pfad angeben kann. Zum Auflösen der Abhängigkeiten von Binärdateien wird im grundlegenden Windows-Anwendungsmodell versucht, die Dateien in einem Suchpfad zu finden. Dies ist eine Liste von Speicherorten, die nacheinander nach einer Datei mit dem verlangten Grundnamen abgesucht werden. Dieser Suchpfadmechanismus kann jedoch von verschiedenen Systemkomponenten überschrieben werden, um das reguläre Anwendungsmodell zu erweitern. Das Prinzip des Suchpfads stammt noch aus dem Zeitalter der Befehlszeile, als der Begriff des »aktuellen Verzeichnisses« einer Anwendung tatsächlich eine sinnvolle Bedeutung hatte. Für moderne GUI-Anwendungen wirkt er dagegen etwas anachronistisch. Da sich das aktuelle Verzeichnis in diesem Suchpfad befand, konnten Angreifer jedoch die Ladeoperationen für Systembinärdateien überschreiben, indem sie schädliche Binärdateien desselben Namens in das aktuelle Verzeichnis der Anwendung stellten. Diese Technik wurde als Binary Planting bezeichnet. Um dieses Sicherheitsrisiko zu vermeiden, wurde die Such­ pfadberechnung um den sicheren DLL-Suchmodus ergänzt, der für alle Prozesse standardmäßig aktiviert ist. In diesem Modus wird das aktuelle Verzeichnis hinter die drei Systemverzeichnisse verlagert, was zu der folgenden Suchpfadreihenfolge führt: 1. Das Verzeichnis, in dem die Anwendung gestartet wurde 2. Das native Windows-Systemverzeichnis (z. B. C:\Windows\System32) 3. Da 16-Bit-Windows-Systemverzeichnis (z. B. C:\Windows\System) 4. Das Windows-Verzeichnis (z. B. C:\Windows) Der Abbildlader Kapitel 3 179
5. Das aktuelle Verzeichnis zum Startzeitpunkt der Anwendung 6. Jegliche in der Umgebungsvariablen %PATH% angegebenen Verzeichnisse Der DLL-Suchpfad wird für jede DLL-Ladeoperation neu berechnet. Der Algorithmus für diese Berechnung ist derselbe wie für den Standardsuchpfad, aber die Anwendung kann einzelne Pfadelemente überschreiben, indem sie die Variable %PATH% mit SetEnvironmentVariable bearbeitet, das aktuelle Verzeichnis mit SetCurrentDirectory ändert oder mit SetDllDirectoryAPI ein DLL-Verzeichnis für den Prozess angibt. In letzterem Fall ersetzt das DLL-Verzeichnis das aktuelle Verzeichnis im Suchpfad. Der Lader ignoriert dann die Einstellung des sicheren DLL-Suchmodus für den Prozess. Aufrufer können den DLL-Suchpfad auch für einzelne Ladeoperationen ändern, indem sie der API LoadLibraryEx das Flag LOAD_WITH_ALTERED_SEARCH_PATH übergeben. Ist dieses Flag gesetzt und enthält der an die API übergebene DLL-Name den String für einen vollständigen Pfad, dann wird bei der Berechnung des Suchpfads für die Operation der Pfad mit der DLL-­Datei anstelle des Anwendungsverzeichnisses verwendet. Bei einem relativen Pfad ist das Verhalten nicht definiert, was gefährlich sein kann. Beim Laden von Desktop-Bridge-Anwendungen (Centennial) wird dieses Flag ignoriert. Neben LOAD_WITH_ALTERED_SEARCH_PATH können Anwendungen auch die Flags LOAD_­ LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_APPLICATION_DIR, LOAD_LIBRARY_SEARCH_ SYSTEM32 und LOAD_LIBRARY_SEARCH_USER_DIRS an LoadLibraryEx übergeben. Sie alle ändern die Suchreihenfolge, sodass nur in den Verzeichnissen gesucht wird, auf die sie verweisen. Beispielsweise wird bei LOAD_LIBRARY_SEARCH_DEFAULT_DIRS im Anwendungsverzeichnis, in System32 und im Benutzerverzeichnis gesucht. Die Flags können auch kombiniert werden, um mehrere Speicherorte zu durchsuchen. Mit SetDefaultDllDirectories können die Flags auch global gesetzt werden, sodass sie sich auf alle zukünftigen Bibliotheksladevorgänge auswirken. Bei Paketanwendungen und bei Anwendungen, die weder ein Paketdienst noch eine veraltete Anwendung für Silverlight 8.0 auf Windows Phone sind, gibt es noch eine weitere Möglichkeit, um die Suchreihenfolge zu beeinflussen. Unter diesen Umständen werden zur DLL-Suche nicht der herkömmliche Mechanismus und die APIs herangezogen, sondern eine paketgestützte Graphensuche. Das gilt auch, wenn statt der regulären Funktion Load­ LibraryEx die API LoadPackagedLibrary verwendet wird. Der paketgestützte Graph basiert auf den <PackageDependency>-Einträgen im Abschnitt <Dependencies> der Manifestdatei einer UWP-Anwendung und garantiert, dass in dem Paket keine willkürlichen DLLs geladen werden können. Beim Laden einer Paketanwendung (mit Ausnahme von Desktop-Bridge-Anwendungen) werden alle von der Anwendung konfigurierbaren APIs zur Änderung des DLL-Suchpfads (wie diejenigen, die Sie zuvor gesehen haben) deaktiviert. Nur das Standardverhalten des Systems wird verwendet (wobei für die meisten UWP-Anwendungen auch wie zuvor beschrieben die Paketabhängigkeiten untersucht werden). Doch selbst mit dem sicheren Suchmodus und den Standardpfad-Suchalgorithmen für ältere Anwendungen, bei denen das Anwendungsverzeichnis immer an erster Stelle steht, ist es leider immer noch möglich, dass eine Binärdatei von ihrem üblichen Speicherort an einen kopiert wird, der für die Benutzer zugänglich ist (beispielsweise von C:\Windows\System32\ Notepad.exe in C:\temp\Notepad.exe, wozu nicht einmal Administratorrechte erforderlich 180 Kapitel 3 Prozesse und Jobs
sind). In einer solchen Situation kann ein Angreifer eine besonders gestaltete DLL im Verzeichnis der Anwendung platzieren, wo sie aufgrund der zuvor aufgezeigten Reihenfolge Vorrang vor der System-DLL hat. Dies lässt sich dann zur Persistenz oder zu einer anderen Beeinflussung der Anwendung nutzen, die möglicherweise höhere Rechte hat (vor allem wenn der Benutzer, der nichts von dem Austausch ahnt, ihre Rechte über die Benutzerkontensteuerung erhöht hat). Als Schutz dagegen können Prozesse und Administratoren die Prozessvorbeugungsrichtlinie Prefer System32 Images nutzen (siehe Kapitel 7), die die Reihenfolge von Punkt 1 und 2 umkehrt. DLL-Namensumleitung Bevor sich der Lader daranmacht, einen DLL-Namensstring zu einer Datei aufzulösen, versucht er, Regeln zur DLL-Namensumleitung anzuwenden. Diese Regeln dienen dazu, Teile des DLL-Namensraums – der gewöhnlich dem Namensraum des Win32-Dateisystems entspricht – zu erweitern oder zu überschreiben, um damit das Windows-Anwendungsmodell zu erweitern. In der Reihenfolge der Anwendung sind dies folgende Regeln: QQ MinWin-API-Set-Umleitung Der Mechanismus der API-Sets beruht auf dem Prinzip von Kontrakten. Damit können verschiedene Versionen oder Editionen von Windows die Binärdatei, die eine bestimmte System-API exportiert, für die Anwendungen transparent ändern. Dieser Mechanismus wurde bereits kurz in Kapitel 2 erwähnt und wird in einem Abschnitt weiter hinten ausführlicher erklärt. QQ .LOCAL-Umleitung Mit dem .LOCAL-Umleitungsmechanismus können Anwendungen alle Ladevorgänge für einen gegebenen DLL-Basisnamen zu einer lokalen Kopie der DLL im Anwendungsverzeichnis umleiten, und zwar unabhängig davon, ob ein vollständiger Pfad angegeben wurde oder nicht. Dazu wird entweder eine Kopie der DLL mit demselben Basisnamen und der Endung .local erstellt (z. B. MyLibrary.dll.local) oder ein Dateiordner namens .local im Anwendungsverzeichnis erstellt und eine Kopie der lokalen DLL dort hineingelegt (z. B. C:\\MyApp\.LOCAL\MyLibrary.dll). Die von diesem Mechanismus umgeleiteten DLLs werden genauso behandelt wie die von SxS umgeleiteten (siehe den nächsten Punkt). Der Lader berücksichtigt die .LOCAL-Umleitung von DLLs nur, wenn die ausführbare Datei kein Manifest hat, weder ein eingebettetes noch ein externes. Diese Regel ist nicht standardmäßig aktiviert. Um sie global einzuschalten, fügen Sie den DWORD-Wert DevOverrideEnable zum IFEO-Schlüssel hinzu (HKLM\Software\Microsoft\ WindowsNT\CurrentVersion\Image File Execution Options) und setzen ihn auf 1. QQ Fusion-Umleitung (SxS) Fusion (auch Side-by-Side oder SxS genannt) ist eine Erweiterung des Windows-Anwendungsmodells, die es Komponenten ermöglicht, ausführlichere Informationen über Abhängigkeiten von Binärdateien auszudrücken (meistens Versionsinformationen). Dazu werden als Manifeste bezeichnete Binärressourcen eingebettet. Der Fusion-Mechanismus wurde ursprünglich verwendet, damit Anwendungen die richtige Version des Pakets Windows Common Controls (Comctl32.dll) laden konnten, nachdem die Binärdatei auf zwei verschiedene Versionen aufgeteilt worden war, die sich nebeneinander installieren ließen. Inzwischen wurde eine ähnliche Versionsverwaltung auch für andere Binärdateien eingeführt. In Visual Studio 2005 mit dem Microsoft-Linker erstellte Der Abbildlader Kapitel 3 181
Anwendungen verwenden Fusion, um die richtige Version der C-Laufzeitbibliotheken zu finden, während ab Visual Studio 2015 eine API-Set-Umleitung eingesetzt wird, um eine universelle CRT umzusetzen. Das Fusion-Laufzeitwerkzeug liest mithilfe des Windows Ressourcenladers die eingebetteten Abhängigkeitsinformationen im Ressourcenabschnitt der Binärdatei und verpackt sie in Nachschlagestrukturen, die als Aktivierungskontexte bezeichnet werden. Beim Systemund Prozessstart erstellt das System Aktivierungskontexte auf System- bzw. Prozessebene. Außerdem ist mit jedem Thread ein Aktivierungskontextstack verknüpft, wobei die Aktivierungskontextstruktur auf seiner Spitze als die aktive angesehen wird. Dieser threadweise Aktivierungskontextstack wird explizit über die APIs ActivateActCtx und DeactivacteActCtx und an einigen Stellen auch implizit vom System verwaltet, etwa wenn die DLL-Hauptroutine einer Binärdatei mit eingebetteten Abhängigkeitsinformationen aufgerufen wird. Beim Nachschlagen einer Fusion-DLL-Namensumleitung sucht das System nach Umleitungsinformationen in dem Aktivierungskontext oben auf dem Aktivierungskontextstack des Threads und danach nach den Prozess- und Systemaktivierungskontexten. Sind solche Umleitungsinformationen vorhanden, wird die in dem Aktivierungskontext angegebene Dateikennung für den Ladevorgang verwendet. QQ Umleitung zu bekannten DLLs (Known-DLL-Umleitung) Dieser Mechanismus ordnet bestimmte DLL-Basisnamen den Dateien im Systemverzeichnis zu, um zu verhindern, dass DLLs durch eine alternative Version an einem anderen Speicherort ersetzt werden. Die DLL-Versionsprüfung für 64-Bit- und Wow64-Anwendungen stellt einen Grenzfall des DLL-Suchpfadalgorithmus dar. Wenn eine DLL mit einem entsprechenden Basisnamen gefunden wurde, nachträglich aber erkannt wird, dass sie für die falsche Architektur kompiliert wurde – z. B. ein 64-Bit-Abbild in einer 32-Bit-Anwendung –, dann ignoriert der Lader den Fehler und nimmt die Suchpfadoperation wieder auf, wobei er mit dem Pfadelement hinter demjenigen beginnt, das zu der falschen Datei geführt hat. Dieses Verhalten erlaubt Anwendungen, in der globalen Umgebungsvariablen %PATH% sowohl 64- als auch 32-Bit-Einträge vorzunehmen. Experiment: Die DLL-Suchreihenfolge beobachten Mit dem Process Monitor von Sysinternals können Sie beobachten, wie der Lader nach DLLs sucht. Wenn er versucht, eine DLL-Abhängigkeit aufzulösen, ruft er CreateFile auf, um jeden Speicherort in der Suchreihenfolge zu sondieren, bis er entweder die angegebene DLL gefunden hat oder der Ladevorgang fehlschlägt. Die nachstehende Abbildung zeigt die Suche des Laders nach der ausführbaren Datei OneDrive.exe. Um dieses Experiment nachzuvollziehen, gehen Sie wie folgt vor: 1. Wenn OneDrive ausgeführt wird, schließen Sie es über sein Symbol in der Taskleiste. Schließen Sie alle Explorer-Fenster, die zurzeit auf OneDrive zugreifen. 2. Öffnen Sie Process Monitor und fügen Sie Filter hinzu, um nur den Prozess OneDrive. exe oder optional nur die CreateFile-Operationen anzuzeigen. → 182 Kapitel 3 Prozesse und Jobs
3. Wechseln Sie zu %LocalAppData%\Microsoft\OneDrive und starten Sie OneDrive.exe oder OneDrivePersonal.cmd (wodurch die Personal- statt der Business-Version von OneDrive gestartet wird). Sie sehen jetzt eine Ausgabe wie die folgende (beachten Sie dabei, dass OneDrive ein 32-Bit-Prozess ist, der hier auf einem 64-Bit-System läuft): Hier sind einige der Aufrufe aus dem zuvor beschriebenen Suchvorgang zu sehen: QQ Bekannte DLLs werden aus dem Systemspeicherort geladen (ole32.dll im Screenshot). QQ LoggingPlatform.dll wird aus einem Versionsunterverzeichnis geladen, wahrscheinlich, weil OneDrive SetDllDirectory aufruft, um Suchvorgänge zur letzten Version umzuleiten (17.3.6743.1212 im Screenshot). QQ Im Verzeichnis der ausführbaren Datei wird erfolglos nach MSVCR120.dll (MSVC-Laufzeitversion 12) gesucht. Anschließend wird die Suche im Versionsunterverzeichnis fortgesetzt, wo sich die DLL tatsächlich befindet. QQ Wsock32.dll (WinSock) wird erst im Pfad der ausführbaren Datei und dann im Versionsunterverzeichnis gesucht und schließlich im Systemverzeichnis gefunden (SysWow64). Beachten Sie, dass es sich hierbei nicht um eine bekannte DLL handelt. Die Datenbank der geladenen Module Der Lader führt eine Liste aller Module (DLLs und die primäre ausführbare Datei), die der Prozess geladen hat. Diese Informationen werden im PEB gespeichert, und zwar in einer Unterstruktur namens PEB_LDR_DATA. Dort unterhält der Lader drei doppelt verknüpfte Listen, die alle dieselben Informationen enthalten, jedoch in unterschiedlicher Anordnung (sortiert nach Laderreihenfolge, Speicherort und Initialisierungsreihenfolge). Die Strukturen in den Listen Der Abbildlader Kapitel 3 183
werden als Laderdaten-Tabelleneinträge (LDR_DATA_TABLE_ENTRY) bezeichnet und enthalten die Informationen über die einzelnen Module. Da Nachschlagevorgänge in verknüpften Listen algorithmisch kostspielig sind (aufgrund ihrer linearen Laufzeit), unterhält der Lader auch zwei Rot-Schwarz-Bäume, bei denen es sich um effiziente binäre Suchbäume handelt. Der erste ist nach Basisadressen sortiert, der zweite nach den Hashes der Modulnamen. Dank dieser Bäume kann der Suchalgorithmus mit logarithmischer Laufzeit ausgeführt werden, was erheblich effizienter ist und die Prozesserstellung in Windows 8 und höher beschleunigt. Als Sicherheitsmaßnahme sind die Wurzeln dieser beiden Bäume anders als verknüpfte Listen im PEB nicht zugänglich. Dadurch sind sie für Shellcode schwerer zu finden, der in einer Umgebung mit randomisiertem Adressraumlayout operiert (Address Space Layout Randomization, ASLR; siehe Kapitel 5). Tabelle 3–9 führt die Informationen auf, die der Lader in einem Eintrag unterhält. Feld Bedeutung BaseAddressIndexNode Verknüpft diesen Eintrag als Knoten mit dem nach Basisadressen sortierten Rot-Schwarz-Baum. BaseDllName/BaseNameHashValue Der Name des Moduls ohne vollständigen Pfad. Das zweite Feld enthält den mit RtlHashUnicodeString ermittelten Hash dieses Namens. DdagNode/NodeModuleLink Ein Zeiger auf die Datenstruktur, die den verteilten Abhängigkeitsgraphen (Distributed Dependency Graph, DDAG) im Auge behält. Dadurch wird das parallele Laden von Abhängigkeiten durch den Arbeitsthreadpool möglich. Das zweite Feld verknüpft die Struktur mit den damit verknüpften Laderdaten-Tabelleneinträgen (Teil desselben Graphen). DllBase Enthält die Basisadresse, an der das Modul geladen wurde. EntryPoint Enthält die ursprüngliche Routine des Moduls (z. B. DllMain). EntryPointActivationContext Enthält den SxS/Fusion-Aktivierungskontext beim Aufrufen von Initialisierern. Flags Laderstatusflags für das Modul (siehe Tabelle 3–10) ForwarderLinks Eine verknüpfte Liste der Module, die aufgrund von Exporttabellenweiterleitungen von dem Modul geladen wurden FullDllName Der vollständig qualifizierte Pfadname des Moduls HashLinks Eine verknüpfte Liste, die beim Starten und Herunterfahren des Prozesses verwendet wird, um das Nachschlagen zu beschleunigen ImplicitPathOptions Dient zur Speicherung der Pfadsuchflags, die mit der API LdrSetImplicitPathOptions festgelegt oder vom DLL-Pfad geerbt werden können. List Entry Links Verknüpft diesen Eintrag mit jeder der drei geordneten Listen in der Laderdatenbank. LoadContext Zeiger auf die aktuellen Laderinformationen über die DLL. Gewöhnlich NULL, sofern nicht aktiv geladen. ObsoleteLoadCount Referenzzähler für das Modul (also die Angabe, wie oft es geladen wurde). Dieser Zähler ist nicht mehr genau und wurde in die DDAG-Knotenstruktur verlagert. → 184 Kapitel 3 Prozesse und Jobs
Feld Bedeutung LoadReason Enthält einen Aufzählungswert, der erklärt, warum die DLL geladen wurde (dynamisch, statisch, als Forwarder, als verzögert geladene Abhängigkeit usw.). LoadTime Speichert den Wert der Systemzeit, zu der das Modul geladen wurde. MappingInfoIndexNode Verknüpft diesen Eintrag als Knoten mit dem nach Namenshashes sortierten Rot-Schwarz-Baum. OriginalBase Speichert die (vom Linker festgelegte) ursprüngliche Basis­ adresse des Moduls vor ASLR oder Relokalisierungen, was eine schnellere Verarbeitung relokalisierter Importeinträge ermöglicht. ParentDllBase Speichert bei statischen, Forwarder- oder verzögert geladenen Abhängigkeiten die Adresse der DLL, die von dieser DLL abhängt. SigningLevel Speichert die Signaturebene des Abbilds (siehe Kapitel 8 in Band 2). SizeOfImage Größe des Moduls im Arbeitsspeicher SwitchBackContext Wird von SwitchBack verwendet (siehe weiter hinten), um den aktuellen Windows-Kontext-GUID für das Modul und andere Daten zu speichern. TimeDateStamp Ein vom Linker geschriebener Zeitstempel, der den Verlinkungszeitpunkt des Moduls angibt. Der Lader ruft diesen Zeitstempel aus dem PE-Abbildheader des Moduls ab. TlsIndex Der TLS-Slot für das Modul Tabelle 3–9 Felder in einem Laderdaten-Tabelleneintrag Eine Möglichkeit, um die Laderdatenbank eines Prozesses zu untersuchen, besteht darin, sich die formatierte PEB-Ausgabe von WinDbg anzusehen. Im nächsten Experiment erfahren Sie, wie das gemacht wird und wie Sie die LDR_DATA_TABLE_ENTRY-Strukturen betrachten. Experiment: Die Datenbank der geladenen Module ausgeben Führen Sie vor Beginn dieses Experiments die gleichen Schritte wie bei den vorherigen beiden Versuchen aus, um Notepad.exe mit WinDbg als Debugger zu starten. Wenn Sie den ersten Haltepunkt erreichen (an dem Sie zuvor immer angewiesen wurden, (g) zu drücken), gehen Sie wie folgt vor: 1. Schauen Sie sich mit dem Befehl !peb den PEB des aktuellen Prozesses an. Zunächst sind wir nur an den angezeigten Ldr-Daten interessiert: 0:000> !peb PEB at 000000dd4c901000 InheritedAddressSpace: No ReadImageFileExecOptions: No BeingDebugged: Yes → Der Abbildlader Kapitel 3 185
ImageBaseAddress: 00007ff720b60000 Ldr 00007ffe855d23a0 Ldr.Initialized: Yes Ldr.InInitializationOrderModuleList: 0000022815d23d30 . 0000022815d24430 Ldr.InLoadOrderModuleList: 0000022815d23ee0 . 0000022815d31240 Ldr.InMemoryOrderModuleList: 0000022815d23ef0 . 0000022815d31250 Base TimeStamp Module 7ff720b60000 5789986a Jul 16 05:14:02 2016 C:\Windows\System32\ notepad.exe 7ffe85480000 5825887f Nov 11 10:59:43 2016 C:\WINDOWS\SYSTEM32\ ntdll.dll 7ffe84bd0000 57899a29 Jul 16 05:21:29 2016 C:\WINDOWS\System32\ KERNEL32.DLL 7ffe823c0000 582588e6 Nov 11 11:01:26 2016 C:\WINDOWS\System32\ KERNELBASE.dll ... 2. Die Adresse, die in der Ldr-Zeile gezeigt wird, ist ein Zeiger auf die zuvor beschriebene PEB_LDR_DATA-Struktur. WinDbg zeigt die Adressen der drei Listen an und gibt die Liste in der Initialisierungsreihenfolge aus, wobei für jedes Modul der vollständige Pfad, der Zeitstempel und die Basisadresse angegeben werden. 3. Sie können auch die einzelnen Moduleinträge jeweils für sich selbst analysieren, indem Sie die Modulliste durchgehen und dann die Daten an jeder Adresse in Form einer LDR_ DATA_TABLE_ENTRY-Struktur ausgeben. Anstatt das für jeden Eintrag selbst zu machen, können Sie sich den Großteil der Arbeit auch von WinDbg abnehmen lassen, indem Sie die Erweiterung !list und die folgende Syntax verwenden: !list –x "dt ntdll!_LDR_DATA_TABLE_ENTRY" @@C++(&@$peb->Ldr>InLoadOrderModuleList) 4. Nun sehen Sie die Einträge für die einzelnen Module: +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000228'15d23d10 - +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000228'15d23d20 - 0x00007ffe'855d23b0 ] 0x00007ffe'855d23c0 ] +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000'00000000 0x00000000'00000000 ] +0x030 DllBase : 0x00007ff7'20b60000 Void +0x038 EntryPoint : 0x00007ff7'20b787d0 Void +0x040 SizeOfImage : 0x41000 +0x048 FullDllName : _UNICODE_STRING "C:\Windows\System32\notepad.exe" +0x058 BaseDllName : _UNICODE_STRING "notepad.exe" 186 +0x068 FlagGroup : [4] "???" +0x068 Flags : 0xa2cc Kapitel 3 Prozesse und Jobs
In diesem Abschnitt geht es um den Benutzermodus-Lader aus Ntdll.dll. Der Kernel aber hat seinen eigenen Lader für Treiber und abhängige DLLs. Dieser Kernelmoduslader hat eine ähnliche Eintragsstruktur, die als KLDR_DATA_TABLE_ENTRY bezeichnet wird, und seine eigene Datenbank aus diesen Einträgen, die über die globale Datenvariable PsActiveModuleList direkt zugänglich ist. Um die Datenbank der geladenen Module des Kernels auszugeben, können Sie einen ähnlichen !list-Befehl wie im vorstehenden Experiment verwenden, wobei Sie allerdings den Zeiger am Ende des Befehls durch nt!PsActiveModuleList ersetzen und den neuen Struktur- bzw. Modulnamen angeben müssen, also !list nt!_KLDR_DATA_TABLE_ENTRY nt!PsActiveModuleList. Wenn Sie sich die Liste in diesem rohen Format ansehen, erhalten Sie einige besondere Einblicke in die Interna des Laders. So sehen Sie dabei beispielsweise das Feld Flags mit Statusinformationen, die Ihnen der reine !peb-Befehl nicht anzeigt. Die Bedeutung dieser Flags finden Sie in Tabelle 3–10. Da sowohl der Kernel- als auch der Benutzermoduslader diese Struktur verwendet, können die Flags unterschiedliche Bedeutung haben. In der Tabelle werden lediglich die Benutzermodusflags beschrieben (von denen einige auch in der Kernelstruktur vorhanden sein können). Flag Bedeutung Packaged Binary (0x1) Das Modul ist Teil einer Paketanwendung (kann nur für das Hauptmodul eines AppX-Pakets gesetzt werden). Marked for Removal (0x2) Das Modul wird entladen, sobald alle Referenzen (z. B. von einem ausführenden Arbeitsthread) entfernt wurden. Image DLL (0x4) Das Modul ist eine Abbild-DLL (also keine Daten-DLL und keine ausführbare Datei). Load Notifications Sent (0x8) Die registrierten DLL-Benachrichtigungscallouts wurden bereits über dieses Abbild benachrichtigt. Telemetry Entry Processed (0x10) Telemetriedaten wurden bereits für dieses Abbild verarbeitet. Process Static Import (0x20) Das Modul ist ein statischer Import der binären Hauptdatei der Anwendung. In Legacy Lists (0x40) Der Abbildeintrag befindet sich in den doppelt verknüpften Listen des Laders. In Indexes (0x80) Der Abbildeintrag befindet sich in den Rot-Schwarz-Bäumen des Laders. Shim DLL (0x100) Der Abbildeintrag steht für den DLL-Teil der Shim-Engine-/ Anwendungskompatibilitätsdatenbank. In Exception Table (0x200) Die .pdata-Ausnahmehandler des Moduls wurden von der invertierten Funktionstabelle des Laders erfasst. Load In Progress (0x800) Das Modul wird zurzeit geladen. Load Config Processed (0x1000) Das Ladekonfigurationsverzeichnis des Moduls wurde gefunden und verarbeitet. Entry Processed (0x2000) Der Lader hat die Verarbeitung des Moduls komplett abgeschlossen. Protect Delay Load (0x4000) ControlFlowGuard-Merkmale für diese Binärdatei haben den Schutz der Ladeverzögerungs-IAT angefordert (siehe Kapitel 7). Der Abbildlader Kapitel 3 → 187
Flag Bedeutung Process Attach Called (0x20000) Die Benachrichtigung DLL_PROCESS_ATTACH wurde bereits an die DLL gesendet. Process Attach Failed (0x40000) Bei der DllMain-Routine der DLL ist die Benachrichtigung DLL_PROCESS_ATTACH fehlgeschlagen. Don't Call for Threads (0x80000) Zu dieser DLL soll keine DLL_THREAD_ATTACH/DETACH-Benachrichtigung gesendet werden. Dieses Flag kann mit Disable­ ThreadLibraryCalls gesetzt werden. COR Deferred Validate (0x100000) Die Common Object Runtime (COR) wird dieses .NET-Abbild später validieren. COR Image (0x200000) Das Modul ist eine .NET-Anwendung. Don't Relocate (0x400000) Das Abbild darf weder relokalisiert noch randomisiert werden. COR IL Only (0x800000) Dies ist eine reine (Abbildlader-) .NET-Zwischensprachenbibliothek, die keinen nativen Assemblycode enthält. Compat Database Processed (0x40000000) Die Shim-Engine hat die DLL verarbeitet. Tabelle 3–10 Flags für Laderdaten-Tabelleneinträge Importanalyse Da Sie jetzt wissen, wie der Lader den Überblick über alle für einen Prozess geladenen Module behält, können Sie nun mit der Analyse der vom Lader durchgeführten Startinitialisierungsaufgaben fortfahren. In diesem Schritt erledigt der Lader Folgendes: 1. Er lädt die in der Importtabelle des ausführbaren Abbilds für den Prozess angegebenen DLLs. 2. Er prüft jeweils, ob die DLL bereits geladen wurde, indem er die Moduldatenbank abfragt. Steht die DLL nicht auf der Liste, öffnet der Lader sie und ordnet sie im Arbeitsspeicher zu. 3. Während der Zuordnung schaut sich der Lader zunächst die verschiedenen Pfade an, in denen die DLL zu finden sein könnte, und prüft, ob es sich um eine bekannte DLL handelt, also um eine, die das System bereits beim Start geladen und für die es eine im Arbeitsspeicher zugeordnete globale Datei für den Zugriff bereitgestellt hat. Es können dabei Abweichungen vom normalen Suchalgorithmus vorkommen, beispielsweise durch eine .local-Datei, die den Lader zwingt, die DLLs im lokalen Pfad zu verwenden, oder eine Manifestdatei, die eine umgeleitete DLL angibt, um die Verwendung einer bestimmten Version zu garantieren. 4. Nachdem der Lader die DLL auf der Festplatte gefunden und zugeordnet hat, prüft er, ob der Kernel sie an anderer Stelle geladen hat (Relokalisierung). Ist das der Fall, durchsucht der Lader die Relokalisierungsinformationen in der DLL und führt die erforderlichen Operationen aus. Stehen keine Relokalisierungsinformationen bereit, schlägt das Laden der DLL fehl. 5. Anschließend erstellt der Lader einen Laderdaten-Tabelleneintrag für die DLL und führt ihn in die Datenbank ein. 188 Kapitel 3 Prozesse und Jobs
6. Nach der Zuordnung einer DLL wird der Vorgang für die DLL wiederholt, um ihre Importtabelle und ihre gesamten Abhängigkeiten zu analysieren. 7. Nachdem alle DLLs geladen wurden, untersucht der Lader die IAT auf besondere Funktionen, die importiert wurden. Dies geschieht gewöhnlich nach Namen, kann aber auch nach Indexzahlen geschehen. Für jeden Namen durchläuft der Lader die Exporttabelle der importierten DLL und versucht, eine Übereinstimmung zu finden. Gibt es keine Übereinstimmung, wird die Operation abgebrochen. 8. Auch die Importtabelle eines Abbilds kann gebunden werden. Das bedeutet, dass die Entwickler zur Verlinkungszeit statische Adressen zuweisen, die auf importierte Funktionen in externen DLLs zeigen. Dadurch entfällt die Notwendigkeit, jeden Namen nachzuschlagen, allerdings wird vorausgesetzt, dass sich die DLLs, auf die die Anwendung zurückgreift, immer an derselben Adresse befinden. Da Windows eine Adressraumrandomisierung durchführt (siehe Kapitel 5), ist dies für Systemanwendungen und Bibliotheken gewöhnlich nicht der Fall. 9. Die Exporttabelle einer importierten DLL kann einen Forwarder-Eintrag haben, was bedeutet, dass die eigentliche Funktion in einer anderen DLL implementiert ist. Dieser Fall muss im Grunde genommen wie ein Import oder eine Abhängigkeit behandelt werden. Nach der Untersuchung der Exporttabelle muss daher jede DLL geladen werden, auf die ein Forwarder verweist. Der Lader kehrt danach zu Schritt 1 zurück. Nachdem alle importierten DLLs (und ihre eigenen Abhängigkeiten oder Importe) geladen, alle erforderlichen importierten Funktionen nachgeschlagen und gefunden und auch alle Forwarder geladen und verarbeitet wurden, ist der Schritt abgeschlossen: Alle von der Anwendung und ihren DLLs zur Kompilierungszeit definierten Abhängigkeiten sind erfüllt. Bei der Ausführung können verzögert geladene Abhängigkeiten und Laufzeitoperationen (wie der Aufruf von LoadLibrary) den Lader aufrufen und praktisch die gleichen Aufgaben erneut ausführen. Wenn diese Schritte beim Start des Prozesses ausgeführt werden, führt ein Fehlschlag dabei jedoch zu einem Fehler beim Starten der Anwendung. Wenn Sie beispielsweise versuchen, eine Anwendung auszuführen, die eine in der aktuellen Version des Betriebssystems nicht vorhandene Funktion benötigt, kann das zu einer Meldung wie der aus Abb. 3–12 führen. Abbildung 3–12 Das Dialogfeld, das erscheint, wenn eine erforderliche (importierte) Funktion in einer DLL nicht vorhanden ist Der Abbildlader Kapitel 3 189
Prozessinitialisierung nach dem Import Nachdem die erforderlichen Abhängigkeiten geladen worden sind, müssen noch verschiedene Initialisierungsaufgaben ausgeführt werden, bevor die Anwendung endlich gestartet werden kann. In dieser Phase erledigt der Lader Folgendes: 1. Diese Schritte beginnen an der Stelle, an der die Variable LdrInitState auf 2 gesetzt wird, was bedeutet, dass die Importe geladen sind. 2. Wenn Sie einen Debugger wie WinDbg benutzen, trifft er schließlich auf den ersten Haltepunkt. Hier müssen Sie wie in den früheren Experimenten (g) drücken, um mit der Ausführung fortzufahren. 3. Es wird geprüft, ob es sich um eine Anwendung des Windows-Teilsystems handelt. Wenn ja, sollte in den ersten Schritten zur Prozessinitialisierung die Funktion BaseThreadInitThunk erfasst worden sein. Jetzt wird sie aufgerufen und auf Erfolg geprüft. Auch die Funktion TermsrvGetWindowsDirectoryW, die (auf einem Gerät mit Unterstützung für Terminaldienste) bereits erfasst worden sein sollte, wird jetzt aufgerufen und setzt den System- und den Windows-Verzeichnispfad zurück. 4. Mithilfe des Verteilungsgraphen durchläuft der Lader rekursiv alle Abhängigkeiten und führt die Initialisierer für alle statischen Importe des Abbilds aus. Bei diesem Schritt wird die Routine DllMain für jede DLL aufgerufen, sodass jede DLL ihre eigenen Initialisierungsarbeiten durchführen kann, wozu auch das Laden weiterer DLLs zur Laufzeit gehören kann. Außerdem werden die TLS-Initialisierer der einzelnen DLLs verarbeitet. Dies ist einer der letzten Schritte, bei denen das Laden einer Anwendung fehlschlagen kann. Sämtliche geladenen DLLs müssen nach dem Abschluss ihrer DllMain-Routinen einen Erfolgscode zurückgeben. Anderenfalls bricht der Lader den Start der Anwendung ab. 5. Falls das Abbild TLS-Slots verwendet, wird sein TLS-Initialisierer aufgerufen. 6. Der Lader führt den Postinitialisierungs-Callback für die Shim-Engine aus, wenn das Modul aus Kompatibilitätsgründen Shims verwendet. 7. Der Lader führt die Postprozessinitialisierungsroutine für die zugehörige Teilsystem-DLL aus, die im PEB registriert ist. Bei Windows-Anwendungen werden dabei beispielsweise Überprüfungen für Terminaldienste ausgeführt. 8. Jetzt wird ein ETW-Ereignis geschrieben, um anzugeben, dass der Prozess erfolgreich geladen wurde. 9. Wenn es eine Mindestzusicherung für den Stack gibt, wird der Threadstack berührt, um eine Einlagerung der zugesicherten Seiten zu erzwingen. 10. LdrInitState wird auf 3 gesetzt, was bedeutet, dass die Initialisierung abgeschlossen ist. Anschließend wird das Feld ProcessInitializing des PEB auf 0 zurückgesetzt und die Variable LdrpProcessInitialized aktualisiert. 190 Kapitel 3 Prozesse und Jobs
SwitchBack In jeder neuen Version von Windows werden Bugs wie Race Conditions und falsche Parametervalidierung in vorhandenen API-Funktionen korrigiert. Wie klein diese Änderungen auch sein mögen, es besteht dabei immer die Gefahr von Inkompatibilitäten mit Anwendungen. Mithilfe der SwitchBack-Technologie, die im Lader implementiert ist, können Softwareentwickler jedoch im Manifest ihrer ausführbaren Dateien einen GUID für die Windows-Version einbetten, für die sie ihre Anwendung geschrieben haben. Nehmen wir an, ein Entwickler möchte die Verbesserungen nutzen, die in Windows 10 zu einer bestimmten API hinzugefügt wurden. Er könnte dann in dem Manifest den GUID von Windows 10 angeben. Bei einer älteren Anwendung, die von einem bestimmten Verhalten von Windows 7 abhängt, würde er dagegen den Windows 7-GUID in das Manifest aufnehmen. SwitchBack durchsucht diese Angaben und vergleicht sie mit den eingebetteten Informa­ tionen in SwitchBack-kompatiblen DLLs (im Abschnitt .sb_data des Abbilds), um zu bestimmen, welche Version der betreffenden API das Modul aufrufen soll. Da SwitchBack auf der Ebene der geladenen Module arbeitet, ist es dadurch möglich, dass in einem Prozess ältere und aktuelle DLLs dieselbe API aufrufen und dabei unterschiedliche Ergebnisse erzielen. SwitchBack-GUIDs Zurzeit sind GUIDs für die Kompatibilität mit allen Versionen ab Windows Vista definiert: QQ {e2011457-1546-43c5-a5fe-008deee3d3f0} für Windows Vista QQ {35138b9a-5d96-4fbd-8e2d-a2440225f93a} für Windows 7 QQ {4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38} für Windows 8 QQ {1f676c76-80e1-4239-95bb-83d0f6d0da78} für Windows 8.1 QQ {8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a} für Windows 10 Diese GUIDs müssen in der Manifestdatei der Anwendung unter dem Abschnitt <SupportedOS> im ID-Attribut des Eintrags für das Kompatibilitätsattribut vorhanden sein. Enthält das Manifest keinen GUID, wird als Standardkompatibilitätsmodus Windows Vista ausgewählt. Auf der Registerkarte Details des Task-Managers können Sie die Spalte Betriebssystemkontext einblenden, in der angezeigt wird, ob eine Anwendung in einem bestimmten Betriebssystemkontext läuft. Ein leerer Wert bedeutet dabei gewöhnlich, dass die Anwendung im Windows 10-Modus ausgeführt wird. Abb. 3–13 zeigt einige Beispielanwendungen, die auf einem Windows 10-System im Windows Vista- bzw. Windows 7-Modus ausgeführt werden. Im folgenden Beispielmanifest ist die Kompatibilität auf Windows 10 festgelegt: <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <application> <!-- Windows 10 --> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> </application> </compatibility> Der Abbildlader Kapitel 3 191
Abbildung 3–13 Prozesse mit abweichenden Kompatibilitätsmodi SwitchBack-Kompatibilitätsmodi Um Ihnen eine Vorstellung davon zu geben, was SwitchBack tun kann, sehen Sie hier, was die Ausführung im Windows 7-Kontext mit sich bringt: QQ RPC-Komponenten verwenden den Windows-Threadpool statt einer privaten Implementierung. QQ Für den primären Puffer kann keine DirectDraw-Sperre eingerichtet werden. QQ Blitting auf dem Desktop ist ohne Clippingfenster nicht erlaubt. QQ Eine Race Condition in GetOverlappedResult ist korrigiert. QQ Aufrufe von CreateFile dürfen ein Herabstufungsflag übergeben, um eine Datei auch dann exklusiv zu öffnen, wenn der Aufrufer keine Schreibrechte hat. Infolgedessen erhält NtCreateFile nicht das Flag FILE_DISALLOW_EXCLUSIVE. Die Ausführung im Windows 10-Modus dagegen betrifft das LFH-Verhalten (Low Fragmentation Heap). Ist kein Windows 10-GUID angegeben, werden alle LFH-Teilsegmente zwangsweise zugesichert und alle Zuweisungen mit einem Headerblock aufgefüllt. In Windows 10 wird 192 Kapitel 3 Prozesse und Jobs
zusätzlich die Vorbeugemaßnahme Raise Exception on Invalid Handle Close (siehe Kapitel 7) verwendet, sodass CloseHandle und RegCloseKey das Verhalten respektieren. Bei früheren Betriebssystemen dagegen wird dieses Verhalten vor dem Aufruf von NtClose deaktiviert und anschließend wieder aktiviert (sofern kein Debugger angeschlossen ist). Ein weiteres Beispiel ist die Einrichtung zur Rechtschreibprüfung, die für Sprachen ohne Rechtschreibprüfung NULL zurückgibt, in Windows 8.1 aber eine »leere« Rechtschreibprüfung. Die Implementierung der Funktion IShellLink::Resolve gibt bei der Übergabe eines relativen Pfads im Windows 8-Kompatibilitätsmodus E_INVALIDARG zurück, wohingegen die entsprechende Überprüfung im Windows 7-Modus nicht stattfindet. Aufrufe von GetVersionEx oder ähnlichen NtDll-Funktionen wie RtlVerifyVersionInfo geben die höchste zurück, die mit der angegebenen SwitchBack-Kontext-GUID übereinstimmt. Diese APIs sind veraltet und mittlerweile unerwünscht. Aufrufe von GetVersionEX geben jetzt auf allen Versionen von Windows 8 und höher 6.2 zurück, sofern kein höherer SwitchBack-GUID angegeben wurde. SwitchBack-Verhalten Bei Änderungen an einer Windows-API, die die Kompatibilität gefährden können, ruft der Eintrittscode die Funktion SbSwitchProcedure auf, um die SwitchBack-Logik zu starten. Dabei übergibt er einen Zeiger auf die SwitchBack-Modultabelle, die Informationen über den von dem Modul genutzten SwitchBack-Mechanismus sowie einen Zeiger auf ein Array mit Einträgen für die einzelnen SwitchBack-Punkte enthält Die Verzweigungspunkte werden dabei mit symbolischen Namen bezeichnet und mit einer ausführlichen Beschreibung und einem zugehörigen Vorbeugetag versehen. Gewöhnlich gibt es in einem Modul mehrere Verzweigungspunkte – einen für Windows Vista-Verhalten, einen für Windows 7-Verhalten usw. Für jeden Verzweigungspunkt wird der erforderliche SwitchBack-Kontext angegeben, der bestimmt, welche der Verzweigungen zur Laufzeit genommen wird. Die einzelnen Deskriptoren enthalten auch jeweils einen Funktionszeiger auf den Code, der in der Verzweigung ausgeführt werden soll. Läuft die Anwendung mit dem Windows 10-GUID, gehört dieser Zeiger zum SwitchBack-Kontext. Bei der Analyse der Modultabelle führt die API SbSelectProcedure einen Abgleich durch. Sie findet den Moduleintragsdeskriptor für den Kontext und ruft den darin enthaltenen Funktionszeiger auf. SwitchBack nutzt ETW, um die Auswahl des SwitchBack-Kontexts und der Verzweigungspunkte zu verfolgen und die Daten in den AIT-Logger von Windows einzuspeisen (Applica­ tion Impact Telemetry). Die Daten können regelmäßig von Microsoft abgefragt werden, um zu bestimmen, in welchem Umfang die einzelnen Kompatibilitätseinträge genutzt werden, die entsprechenden Anwendungen zu identifizieren (das Protokoll enthält eine vollständige Stack­ ablaufverfolgung) und Drittanbieterhersteller zu informieren. Wie bereits erwähnt, wird die Kompatibilitätsstufe der Anwendung in deren Manifest gespeichert. Wenn der Lader die Manifestdatei zum Ladezeitpunkt analysiert, erstellt er eine Kontextdatenstruktur und speichert sie im Element pShimData des PEB zwischen. Diese Kontextdaten enthalten die Kompatibilitäts-GUIDs, mit denen der Prozess ausgeführt wird, und Der Abbildlader Kapitel 3 193
bestimmen, welche Version der Verzweigungspunkte in den aufgerufenen SwitchBack-fähigen APIs ausgeführt wird. API-Sets SwitchBack verwendet eine API-Umleitung, um für Anwendungskompatibilität zu sorgen. Es gibt jedoch auch einen allgemeineren Umleitungsmechanismus, der in Windows für alle Anwendungen verwendet wird – die API-Sets. Sie ermöglichen eine feine Zuordnung von Windows-APIs zu Teil-DLLs, sodass es nicht nötig ist, riesige Mehrzweck-DLLs mit Tausenden von APIs zu handhaben, die bei den heutigen und zukünftigen Windows-Systemen nicht alle erforderlich sind. Diese Technologie wurde hauptsächlich entwickelt, um das Refactoring der untersten Schichten der Windows-Architektur zu ermöglichen und diese besser von den höheren Schichten zu trennen. Sie geht Hand in Hand mit der Aufteilung von u. a. Kernel32.dll und Advapi32.dll in mehrere virtuelle DLL-Dateien. So zeigt der Dependency-Walker-Screenshot aus Abb. 3–14, wie die Windows-Kernbibliothek Kernel32.dll Funktionen aus vielen anderen DLLs importiert, beginnend mit API-MS-WIN. Jede dieser DLLs enthält einen kleinen Teil der APIs, die Kernel32 normalerweise bereitstellt. Zusammen bilden sie die gesamte API-Oberfläche, die von Kernel32.dll verfügbar gemacht wird. Beispielsweise trägt die Bibliothek CORE-STRING nur die grundlegenden Stringfunktionen von Windows bei. Abbildung 3–14 API-Sets für Kernel32.dll 194 Kapitel 3 Prozesse und Jobs
Durch die Aufteilung der Funktionen auf mehrere Dateien werden zwei Ziele erreicht: Erstens sind künftige Anwendungen dadurch in der Lage, eine Verlinkung nur mit den API-Bibliotheken herzustellen, deren Funktionen sie tatsächlich benötigen. Zweitens: Nehmen wir an, Microsoft wollte eine Windows-Version herausgeben, die eine bestimmte Funktionalität nicht unterstützt, beispielsweise die Lokalisierung (etwa ein rein englisches eingebettetes System ohne Benutzerinteraktion). In diesem Fall könnte das Unternehmen einfach die entsprechenden Teil-DLLs herausnehmen und das Schema der API-Sets ändern. Dadurch würde die Binärdatei Kernel32 kleiner, und jegliche Anwendungen, die keine Lokalisierung benötigen, könnten immer noch ausgeführt werden. Mithilfe dieser Technologie wurde das »grundlegende« Windows-System MinWin definiert (und der Quellcode dafür erstellt). Es enthält nur ein Minimum an Diensten, zu denen der Kernel und die Kerntreiber gehören (einschließlich Dateisystemen, grundlegenden Systemprozessen wie Csrss und dem Dienststeuerungs-Manager sowie einer Handvoll Windows-Dienste). Windows Embedded und der Platform Builder bieten eine scheinbar ähnliche Technologie, da Systementwickler hier in der Lage sind, einzelne Windows-Komponenten wie die Shell oder den Netzwerkstack zu entfernen. Dadurch aber bleiben hängende Abhängigkeiten zurück, also Codepfade, die fehlschlagen, wenn sie ausgeführt werden, da sie die entfernten Komponenten benötigen. Die Abhängigkeiten von MinWin sind dagegen in sich abgeschlossen. Bei der Initialisierung ruft der Prozess-Manager die Funktion PspInitializeApiSetMap auf. Sie ist dafür zuständig, das Abschnittsobjekt der API-Set-Umleitungstabelle zu erstellen, die in %SystemRoot%\System32\ApiSetSchema.dll gespeichert ist. Diese DLL enthält keinen ausführbaren Code, verfügt aber über den Abschnitt .apiset mit den Daten zur Zuordnung der virtuellen API-Set-DLLs zu den logischen DLLs, die die APIs implementieren. Wird ein neuer Prozess gestartet, ordnet der Prozess-Manager das Abschnittsobjekt im Prozessadressraum zu und richtet das Feld ApiSetMap im PEB des Prozesses so ein, dass es auf die Basisadresse dieser Zuordnung zeigt. Die Laderfunktion LdrpApplyFileNameRedirection, die normalerweise für die weiter vorn beschriebene .local- und SxS/Fusion-Manifestumleitung zuständig ist, führt auch eine Überprüfung auf API-Set-Umleitungsdaten durch, wenn eine neue Importbibliothek geladen wird (dynamisch oder statisch), deren Name mit API beginnt. Die API-Set-Tabelle ist nach Bibliotheken geordnet, wobei jeder Eintrag beschreibt, in welcher logischen DLL die Funktion gefunden werden kann. Diese DLL ist es, die geladen wird. Die Schemadaten liegen zwar im Binärformat vor, doch mit dem Sysinternals-Programm Strings können Sie die zugehörigen Strings ausgeben, um sich anzusehen, welche DLLs zurzeit definiert sind: C:\Windows\System32>strings apisetschema.dll ... api-ms-onecoreuap-print-render-l1-1-0 printrenderapihost.dllapi-ms-onecoreuap-settingsync-status-l1-1-0 settingsynccore.dll api-ms-win-appmodel-identity-l1-2-0 kernel.appcore.dllapi-ms-win-appmodel-runtime-internal-l1-1-3 api-ms-win-appmodel-runtime-l1-1-2 api-ms-win-appmodel-state-l1-1-2 Der Abbildlader Kapitel 3 195
api-ms-win-appmodel-state-l1-2-0 api-ms-win-appmodel-unlock-l1-1-0 api-ms-win-base-bootconfig-l1-1-0 advapi32.dllapi-ms-win-base-util-l1-1-0 api-ms-win-composition-redirection-l1-1-0 ... api-ms-win-core-com-midlproxystub-l1-1-0 api-ms-win-core-com-private-l1-1-1 api-ms-win-core-comm-l1-1-0 api-ms-win-core-console-ansi-l2-1-0 api-ms-win-core-console-l1-1-0 api-ms-win-core-console-l2-1-0 api-ms-win-core-crt-l1-1-0 api-ms-win-core-crt-l2-1-0 api-ms-win-core-datetime-l1-1-2 api-ms-win-core-debug-l1-1-2 api-ms-win-core-debug-minidump-l1-1-0 ... api-ms-win-core-firmware-l1-1-0 api-ms-win-core-guard-l1-1-0 api-ms-win-core-handle-l1-1-0 api-ms-win-core-heap-l1-1-0 api-ms-win-core-heap-l1-2-0 api-ms-win-core-heap-l2-1-0 api-ms-win-core-heap-obsolete-l1-1-0 api-ms-win-core-interlocked-l1-1-1 api-ms-win-core-interlocked-l1-2-0 api-ms-win-core-io-l1-1-1 api-ms-win-core-job-l1-1-0 ... Jobs Ein Job (oder Auftrag) ist ein Kernelobjekt, das benannt, abgesichert und gemeinsam genutzt werden kann und es ermöglicht, eine Gruppe von Prozessen als eine Einheit zu verwalten und zu bearbeiten. Ein Prozess kann zwar zu beliebig vielen Jobs gehören, ist gewöhnlich aber nur Element eines einzigen. Die Verbindung eines Prozesses mit einem Job kann nicht aufgehoben werden, und alle von dem Prozess und seinen Abkömmlingen erstellten Prozesse werden mit demselben Jobobjekt verknüpft (es sei denn, die Kindprozesse wurden mit dem Flag CREATE_ BREAKAWAY_FROM_JOB erstellt und der Job hat dies nicht eingeschränkt). Das Jobobjekt zeichnet auch grundlegende Buchführungsinformationen für alle mit ihm verbundenen Prozesse auf, einschließlich derer, die bereits beendet wurden. 196 Kapitel 3 Prozesse und Jobs
Jobs können auch mit einem E/A-Abschlussportobjekt verbunden werden, auf das andere Threads mit der Windows-Funktion GetQueuedCompletionStatus oder durch Verwendung der Threadpool-API warten (native Funktion TpAllocJobNotification). Dadurch können daran interessierte Parteien (gewöhnlich der Ersteller des Jobs) nach Verletzungen der Grenzwerte und nach Ereignissen suchen, die die Sicherheit des Jobs gefährden, z. B. nach der Schaffung eines neuen Prozesses oder der unnatürlichen Beendigung eines Prozesses. Jobs spielen bei verschiedenen Systemmechanismen eine wichtige Rolle: QQ Sie verwalten moderne Apps (UWP-Prozesse), wie in Kapitel 9 von Band 2 noch genauer erklärt wird. Jede moderne App wird in einem Job ausgeführt. Das können Sie sich mithilfe von Process Explorer ansehen, was im Experiment »Das Jobobjekt anzeigen« weiter hinten in diesem Kapitel erklärt wird. QQ Sie dienen zur Implementierung der Windows-Unterstützung für Container. Das geschieht mithilfe des Mechanismus der Serversilos, der weiter hinten in diesem Abschnitt beschrieben wird. QQ Sie bilden die wichtigste Einrichtung, durch die der Desktop Activity Moderator (DAM) Drosselung, Timervirtualisierung, das Einfrieren von Timern und andere Leerlaufverhalten für Win32-Anwendungen und -Dienste verwaltet. Der DAM wird in Kapitel 8 von Band 2 beschrieben. QQ Sie ermöglichen die Definition und Verwaltung von Planungsgruppen für die in Kapitel 4 beschriebene dynamische gleichmäßige Planung (Dynamic Fair-Share Scheduling, DFSS). QQ Sie ermöglichen die Angabe einer benutzerdefinierten Speicherpartition, die wiederum die Verwendung der in Kapitel 5 beschriebenen API zur Speicherpartitionierung erlaubt. QQ Sie dienen als entscheidende Voraussetzung für Merkmale wie Ausführen als (sekundäre Anmeldung), Anwendungs-Boxing und den Programmkompatibilitäts-Assistenten. QQ Sie stellen einen Teil der Sicherheitssandbox für Anwendungen wie Google Chrome und den Microsoft Office-Dokumentkonverter sowie einen Schutz gegen DoS-Angriffe durch WMI-Anforderungen (Windows Management Instrumentation) bereit. Grenzwerte für Jobs Für einen Job können Sie unter anderem die folgenden CPU-, Speicher- und E/A-Grenzwerte festlegen: QQ Maximale Anzahl aktiver Prozesse Schränkt die Anzahl der gleichzeitig vorhandenen Prozesse im Job ein. Sobald dieser Grenzwert erreicht ist, wird die Erstellung neuer Prozesse, die diesem Job zugewiesen werden sollen, blockiert. QQ Grenzwert für die CPU-Zeit im Benutzermodus für den gesamten Job Schränkt die CPU-Zeit im Benutzermodus ein, die die Prozesse im Job verbrauchen dürfen (einschließlich der Prozesse, die bereits ausgeführt wurden und beendet sind). Sobald der Grenzwert erreicht ist, werden standardmäßig alle Prozesse im Job mit einem Fehlercode beendet. Es ist dann auch nicht mehr möglich, neue Prozesse in dem Job zu erstellen (sofern der Grenzwert nicht zurückgesetzt wird). Es wird ein Signal über dieses Jobobjekt Jobs Kapitel 3 197
gesendet, damit jegliche Threads, die auf den Job warten, freigegeben werden können. Dieses Standardverhalten können Sie mit einem Aufruf von EndOfJobTimeAction in der Struktur JOBOBJECT_END_OF_JOB_TIME_INFORMATION ändern, die mit der Informationsklasse JobObjectEndOfJobTimeInformation übergeben wird, sodass stattdessen eine Benachrichtigung durch den Abschlussport des Jobs gesendet wird. QQ Grenzwert für die CPU-Zeit im Benutzermodus für einzelne Prozesse Ist dieser Wert angegeben, können die einzelnen Prozesse im Job nur jeweils begrenzte CPU-Zeit im Benutzermodus verbrauchen. Ist der Grenzwert erreicht, wird der Prozess beendet, wobei es keine Möglichkeiten für Aufräumarbeiten mehr gibt. QQ Prozessoraffinität des Jobs Richtet die Prozessoraffinitätsmaske für jeden Prozess im Job ein. Einzelne Threads können ihre Affinität auf eine Teilmenge der Jobaffinität einschränken, doch Prozesse können ihre Prozessaffinitätseinstellungen nicht ändern. QQ Gruppenaffinität des Jobs Stellt eine Liste von Gruppen auf, denen die Prozesse im Job zugeordnet werden können. Affinitätsänderungen erfolgen dann durch Auswahl der entsprechenden Gruppen. Diese Angabe dient als gruppengesteuerte Version der (veralteten) Festlegung der Jobprozessoraffinität und verhindert, dass Letztere verwendet wird. QQ Prioritätsklasse der Jobprozesse Legt die Prioritätsklasse für jeden Prozess im Job fest. Anders als sonst können Threads ihre Priorität dabei nicht erhöhen. Jegliche Versuche dazu werden ignoriert. Bei einem Aufruf von SetThreadPriority wird zwar kein Fehler zurückgegeben, doch die Priorität wird nicht erhöht. QQ Standardminimum und -maximum für den Arbeitssatz Legt das Minimum und Maximum des Arbeitssatzes für jeden Prozess im Job fest. Dies ist jedoch keine jobweite Einstellung: Jeder Prozess hat seinen eigenen Arbeitssatz, wobei für alle Arbeitssätze dieselben Minimum- und Maximumwerte gelten. QQ Grenzwert für den zugesicherten virtuellen Speicher der Prozesse und des Jobs Legt die Höchstmenge des virtuellen Adressraums fest, der von einem einzelnen Prozess oder dem gesamten Job zugesichert werden kann. QQ Steuerung der CPU-Rate Legt das Maximum an CPU-Zeit fest, das der Job benutzen darf, bevor er gedrosselt wird. Diese Einstellung wird für die in Kapitel 4 beschriebene Unterstützung von Planungsgruppen verwendet. QQ Steuerung der Netzwerkbandbreitenrate Legt das Maximum an Bandbreite fest, das dem gesamten Job für ausgehende Verbindungen zur Verfügung steht, bevor eine Drosselung auftritt. Damit kann für die von dem Job gesendeten Netzwerkpakete auch ein DSCP-Wert (Differentiated Services Code Point) für QoS-Zwecke festgelegt werden. Dies lässt sich nur für einen Job in einer Hierarchie einstellen und wirkt sich auf den Job und jegliche Kindjobs aus. QQ Steuerung der Bandbreitenrate für Festplatten-E/A Entspricht der Steuerung der Netzwerkbandbreitenrate, allerdings für Festplatten-E/A. Damit lässt sich die Bandbreite selbst oder die Anzahl der E/A-Operationen pro Sekunde (IOPS) steuern. Die Einstellung kann für ein bestimmtes Volume oder für alle Volumes auf dem System vorgenommen werden. 198 Kapitel 3 Prozesse und Jobs
Für viele dieser Einstellungen kann der Besitzer des Jobs Schwellenwerte angeben, bei deren Erreichen eine Benachrichtigung gesendet wird. (Ist keine Benachrichtigung registriert, wird der Job einfach beendet.) Dazu werden die Benachrichtigungen in eine Warteschlange am E/A-Abschlussport des Jobs gestellt (siehe Dokumentation des Windows SDK). Bei den Ratensteuerungswerten können auch Toleranzbereiche mit Intervallen angegeben werden, sodass ein Prozess beispielsweise alle fünf Minuten bis zu zehn Sekunden lang den Grenzwert für die Netzwerkbandbreite um 20 % überschreiten darf. Für die Prozesse in einem Job können Sie auch Einschränkungen in Bezug auf die Benutzeroberfläche einrichten. Beispielsweise können Sie Prozesse daran hindern, Handles für Fenster zu öffnen, die Threads außerhalb des Jobs gehören, in der Zwischenablage zu lesen und zu schreiben und Systemparameter der Benutzerschnittstelle über die Windows-Funk­ tion SystemParametersInfo zu ändern. Diese Einschränkungen werden vom GDI/USER-Treiber Win32k.sys des Windows-Teilsystems verwaltet und durch den Job-Callout durchgesetzt, den dieser Treiber im Prozess-Manager registriert. Um allen Prozessen in einem Job Zugriff auf bestimmte Benutzerhandles (z. B. Fensterhandles) zu gewähren, können Sie die Funktion UserHandleGrantAccess aufrufen. Sie kann jedoch (logischerweise) nur von einem Prozess aufgerufen werden, der nicht zu dem betreffenden Job gehört. Umgang mit Jobs Ein Jobobjekt erstellen Sie mit der API CreateJobObject. Zu Anfang befinden sich keinerlei Prozesse in dem Job. Um ihn mit einem Prozess zu versehen, rufen Sie AssignProcessToJobObject auf. Sie kann mehrmals verwendet werden, um einem Job mehrere Prozesse oder einen Prozess zu mehreren Jobs hinzuzufügen. Bei der zweiten Möglichkeit entsteht ein verschachtelter Job, was wir uns im nächsten Abschnitt ansehen werden. Sie können einen Prozess auch dadurch zu einem Job hinzufügen, dass Sie mit dem weiter vorn behandelten Prozesserstellungsattribut PS_CP_JOB_LIST manuell ein Handle zu dem Jobobjekt angeben. Dabei können Sie auch Handles für mehrere Jobobjekte angeben, die alle verknüpft werden. Die interessanteste API für Jobs ist SetInformationJobObject, mit der Sie die im vorherigen Abschnitt erwähnten Grenzwerte und Einstellungen einrichten können. Des Weiteren umfasst sie die internen Informationsklassen, die von Mechanismen wie Containern (Silos), dem DAM oder Windows-UWP-Anwendungen genutzt werden. Diese Werte können mit QueryInformationJob­Object wieder gelesen werden, um daran interessierten Parteien die für den Job festgelegten Grenzwerte mitzuteilen. Diese Funktion muss auch aufgerufen werden, wenn Benachrichtigungen über Grenzwerte eingerichtet sind, damit der Aufrufer genau erfährt, welche Grenzwerte verletzt wurden. Ebenfalls oft nützlich ist die Funktion TerminateJobObject, die alle Prozesse in dem Job beendet (so, als wäre TerminateProcess für jeden einzelnen dieser Prozesse aufgerufen worden). Verschachtelte Jobs Bis Windows 7 und Windows Server 2008 R2 konnte ein Prozess immer nur mit einem einzigen Job verbunden sein. Dadurch waren Jobs weniger nützlich, als sie hätten sein können, da eine Anwendung manchmal im Voraus gar nicht wissen konnte, ob ein Prozess, den sie verwalten Jobs Kapitel 3 199
musste, zu einem Job gehörte oder nicht. Ab Windows 8 und Windows Server 2012 kann ein Prozess jedoch mit mehreren Jobs verbunden sein, wodurch im Grunde genommen eine Jobhierarchie entsteht. Ein Kindjob unterhält einen Teil der Prozesse seines Elternjobs. Sobald ein Prozess zu mehr als einem Job hinzugefügt wurde, versucht das System, eine Hierarchie zu bilden. Zurzeit ist der Aufbau einer solchen Hierarchie nicht möglich, falls einer der fraglichen Jobs Einschränkungen für die Benutzeroberfläche festlegt (SetInformationJobObject mit dem Argument JobObjectBasicUIRestrictions). Die Einschränkungen eines Kindjobs können restriktiver sein als die seines Elternjobs, aber nicht weniger streng. Wenn beispielsweise ein Elternjob einen Speichergrenzwert von 100 MB hat, dann kann ein Kindjob für seine Prozesse (und seine eigenen Kindjobs) keinen höheren Grenzwert festlegen (entsprechende Anforderungen schlagen einfach fehl), aber durchaus einen niedrigeren, z. B. 80 MB. Benachrichtigungen an den E/A-Abschlussport des Jobs werden an den Job und alle seine Vorfahren gesendet. Damit eine Benachrichtigung an die Vorfahrenjobs gesendet wird, muss der Job selbst nicht über einen E/A-Abschlussport verfügen. Die Ressourcen für einen Elternjob schließen die gesammelten Ressourcen ein, die von seinen direkt verwalteten Prozessen sowie sämtlichen Prozessen in Kindjobs genutzt werden. Wird ein Job beendet (TerminateJobObject), dann werden alle Prozesse in ihm und in seinen Kindjobs beendet, wobei der Vorgang bei den Kindjobs am unteren Ende der Hierarchie beginnt. Abb. 3–15 zeigt vier Prozesse, die in einer Jobhierarchie verwaltet werden. Abbildung 3–15 Eine Jobhierarchie Um diese Hierarchie zu erstellen, müssen die Prozesse vom Stammjob aus zu den einzelnen Jobs hinzugefügt werden: 1. Prozess 1 wird zu Job 1 hinzugefügt. 2. Prozess 2 wird zu Job 2 hinzugefügt. Dadurch entsteht die erste Verschachtelung. 3. Prozess 2 wird zu Job 1 hinzugefügt. 4. Prozess 2 wird zu Job 3 hinzugefügt. Dadurch entsteht die zweite Verschachtelung. 5. Prozess 3 wird zu Job 2 hinzugefügt. 6. Prozess 4 wird zu Job 1 hinzugefügt. 200 Kapitel 3 Prozesse und Jobs
Experiment: Das Jobobjekt anzeigen Sie können sich benannte Jobobjekte in der Leistungsüberwachung ansehen. Halten Sie dabei nach dem Kategorien Auftragsobjekt und Auftragsobjektdetails Ausschau. Unbenannte Jobs können Sie mit den Befehlen !job und dt nt!_ejob des Kerneldebuggers einsehen. Ob ein Prozess mit einem Job verbunden ist, können Sie sich mithilfe des Kerneldebuggerbefehls !process und in Process Explorer ansehen. Um ein unbenanntes Jobobjekt zu erstellen und anzuzeigen, gehen Sie wie folgt vor: 1. Führen Sie an einer Eingabeaufforderung den Befehl runas aus, um einen Prozess zu erstellen, der innerhalb der Eingabeaufforderung (Cmd.exe) läuft, z. B. runas /user:​ ­<Domäne>\<Benutzername> cmd. 2. Sie werden nach Ihrem Kennwort gefragt. Nachdem Sie es eingegeben haben, erscheint ein Fenster mit einer Eingabeaufforderung. Der Windows-Dienst zur Ausführung von runas-Befehlen erstellt einen unbenannten Job, der alle Prozesse enthält (damit er diese Prozesse bei der Abmeldung beenden kann). 3. Starten Sie Process Explorer, öffnen Sie das Menü Options, wählen Sie Configure Colors und aktivieren Sie den Eintrag Jobs. Der Prozess Cmd.exe und sein Kindprozess ConHost.exe werden als Teil eines Jobs hervorgehoben: 4. Doppelklicken Sie auf den Prozess Cmd.exe oder ConHost.exe, um sein Eigenschaftendialogfeld zu öffnen. Klicken Sie dort auf die Registerkarte Job, um sich Informationen über den Job anzusehen, zu dem der Prozess gehört: → Jobs Kapitel 3 201
5. Führen Sie an der Befehlszeile Notepad.exe aus. 6. Öffnen Sie den Prozess für den Editor und rufen Sie dessen Registerkarte Job auf. Wie Sie sehen, läuft der Editor in demselben Job wie die Eingabeaufforderung, da Cmd.exe nicht das Flag CREATE_BREAKAWAY_FROM_JOB verwendet. Bei verschachtelten Jobs zeigt die Registerkarte Job die Prozesse in dem Job an, zu dem der Prozess unmittelbar gehört, sowie in allen Kindjobs. 7. Führen Sie den Kerneldebugger auf einem aktiven System aus und geben Sie den Befehl !process ein, um Notepad.exe zu finden und die folgenden grundlegenden Informationen darüber anzuzeigen: lkd> !process 0 1 notepad.exe PROCESS ffffe001eacf2080 SessionId: 1 Cid: 3078 Peb: 7f4113b000 ParentCid: 05dc DirBase: 4878b3000 ObjectTable: ffffc0015b89fd80 HandleCount: 188. Image: notepad.exe ... 8. BasePriority 8 CommitCharge 671 Job ffffe00189aec460 Beachten Sie den von null verschiedenen Jobzeiger. Um eine Übersicht über den Job zu bekommen, geben Sie den Debuggerbefehl !job ein: lkd> !job ffffe00189aec460 Job at ffffe00189aec460 Basic Accounting Information TotalUserTime: 0x0 TotalKernelTime: 0x0 TotalCycleTime: 0x0 ThisPeriodTotalUserTime: 0x0 ThisPeriodTotalKernelTime: 0x0 TotalPageFaultCount: 0x0 TotalProcesses: 0x3 ActiveProcesses: 0x3 FreezeCount: 0 BackgroundCount: 0 TotalTerminatedProcesses: 0x0 PeakJobMemoryUsed: 0x10db PeakProcessMemoryUsed: 0xa56 Job Flags Limit Information (LimitFlags: 0x0) Limit Information (EffectiveLimitFlags: 0x0) → 202 Kapitel 3 Prozesse und Jobs
9. Wie Sie sehen, ist ActiveProcesses auf 3 gesetzt (Cmd.exe, ConHost.exe und Notepad. exe). Um sich eine Liste der Prozesse anzusehen, die Teil des Jobs sind, geben Sie hinter dem Befehl !job das Flag 2 an: lkd> !job ffffe00189aec460 2 ... Processes assigned to this job: PROCESS ffff8188d84dd780 SessionId: 1 Cid: 5720 Peb: 43bedb6000 ParentCid: 13cc DirBase: 707466000 ObjectTable: ffffbe0dc4e3a040 HandleCount: <Data Not Accessible> Image: cmd.exe PROCESS ffff8188ea077540 SessionId: 1 Cid: 30ec Peb: dd7f17c000 ParentCid: 5720 DirBase: 75a183000 ObjectTable: ffffbe0dafb79040 HandleCount: <Data Not Accessible> Image: conhost.exe PROCESS ffffe001eacf2080 SessionId: 1 Cid: 3078 Peb: 7f4113b000 ParentCid: 05dc DirBase: 4878b3000 ObjectTable: ffffc0015b89fd80 HandleCount: 188. Image: notepad.exe 10. Sie können auch den Befehl dt verwenden, um sich das Jobobjekt und zusätzliche Felder mit Informationen über den Job anzusehen, z. B. seine Elementebene und seine Beziehungen zu anderen Jobs (Elternjob, Geschwisterjobs und Stammjob). lkd> dt nt!_ejob ffffe00189aec460 +0x000 Event : _KEVENT +0x018 JobLinks : _LIST_ENTRY [ 0xffffe001'8d93e548 - 0xffffe001'df30f8d8 ] +0x028 ProcessListHead : _LIST_ENTRY [ 0xffffe001'8c4924f0 0xffffe001'eacf24f0 ] +0x038 JobLock : _ERESOURCE +0x0a0 TotalUserTime : _LARGE_INTEGER 0x0 +0x0a8 TotalKernelTime : _LARGE_INTEGER 0x2625a +0x0b0 TotalCycleTime : _LARGE_INTEGER 0xc9e03d ... +0x0d4 TotalProcesses : 4 +0x0d8 ActiveProcesses : 3 +0x0dc TotalTerminatedProcesses : 0 ... → Jobs Kapitel 3 203
+0x428 ParentJob : (null) +0x430 RootJob : 0xffffe001'89aec460 _EJOB ... +0x518 EnergyValues : 0xffffe001'89aec988 _PROCESS_ENERGY_VALUES +0x520 SharedCommitCharge : 0x5e8 Windows-Container (Serversilos) Das Aufkommen des billigen, allgegenwärtigen Cloudcomputings hat zu einer weiteren erheblichen Umwälzung des Internets geführt. Für den Aufbau von Onlinediensten und Back-EndServern für mobile Anwendungen ist jetzt kaum mehr erforderlich, als bei einem der vielen Cloudanbieter auf eine Schaltfläche zu klicken. Allerdings hat sich die Konkurrenz zwischen diesen Anbietern verschärft, und der Bedarf für einen Wechsel von einem Cloudanbieter zu einem anderen, von einem Cloudanbieter zu einem Rechenzentrum oder von einem Rechenzentrum zu einem persönlichen High-End-Server ist gestiegen. Aufgrund dieser Entwicklung werden portierbare Back-Ends immer wichtiger, die nach Bedarf bereitgestellt und verlagert werden können, ohne dafür den Mehraufwand in Kauf nehmen zu müssen, den die Ausführung in einer virtuellen Maschine mit sich bringt. Um diesen Bedarf zu befriedigen, wurden Technologien wie Docker entwickelt. Sie ermöglichen die Verlagerung einer »Paketanwendung« von einer Linux-Distribution auf eine andere, ohne sich dabei um die Feinheiten der Bereitstellung einer lokalen Installation oder um den Ressourcenverbrauch einer virtuellen Maschine sorgen zu müssen. Dies war ursprünglich eine reine Linux-Technologie, doch im Rahmen des Anniversary-Updates hat Microsoft Docker auch für Windows 10 verfügbar gemacht. Dabei gibt es zwei verschiedene Betriebsmodi: QQ Durch Bereitstellung einer Anwendung in einem schweren, aber vollständig isolierten Hyper-V-Container, was sowohl für Server als auch für Clients möglich ist QQ Durch Bereitstellung einer Anwendung in einem schlanken, betriebssystemisolierten Silocontainer, was aus Lizenzgründen nur für Server möglich ist In diesem Abschnitt sehen wir uns den zweiten Fall an. Um dies möglich zu machen, mussten tief greifende Änderungen am Betriebssystem vorgenommen werden. Clientsysteme haben zwar prinzipiell auch die Möglichkeit, Serversilocontainer zu erstellen, doch diese Funktion ist zurzeit deaktiviert. Im Gegensatz zu Hyper-V-Containern, die auf eine echte virtualisierte Umgebung zurückgreifen, bietet ein Serversilocontainer eine zweite »Instanz« aller Benutzermoduskomponenten, läuft aber oberhalb desselben Kernels und derselben Treiber. Dadurch kommt eine weit schlankere Containerumgebung zustande, was aber auf Kosten der Sicherheit geht. Jobobjekte und Silos Die Möglichkeiten zum Erstellen eines Silos hängen von mehreren nicht dokumentierten Unterklassen ab, die zur API SetJobObjectInformation gehören. Ein Silo ist also im Grunde genom- 204 Kapitel 3 Prozesse und Jobs
men ein Superjob mit zusätzlichen Regeln und Fähigkeiten. Ein Jobobjekt kann nicht nur für die bisher betrachteten Zwecke der Isolierung und des Ressourcenmanagements verwendet werden, sondern auch, um einen Silo zu erstellen. Solche Jobs werden als Hybridjobs bezeichnet. Jobobjekte können zwei Arten von Silos beherbergen, nämlich Anwendungssilos (die zurzeit zur Implementierung von Desktop Bridge verwendet werden und in Kapitel 9 von Band 2 erläutert werden) und Serversilos. Letztere ermöglichen die Verwendung von Docker-Containern. Siloisolierung Das erste Element zur Definition eines Serversilos ist ein benutzerdefiniertes Stammverzeichnisobjekt (\) des Objekt-Managers. (Der Objekt-Manager wird in Kapitel 8 von Band 2 behandelt.) Diesen Mechanismus haben Sie zwar noch nicht kennengelernt, aber wir wollen hier nur erwähnen, dass sich alle für die Anwendung sichtbaren benannten Objekte (wie Dateien, Regis­ trierungsschlüssel, Ereignisse, Mutexe, RPC-Ports usw.) in einem Stammnamensraum befinden. Dadurch können Anwendungen solche Objekte erstellen, finden und gemeinsam nutzen. Da ein Serversilo seinen eigenen Stamm haben kann, ist es möglich, den Zugriff auf jegliche benannten Objekte zu steuern. Dies kann auf dreierlei Weise geschehen: QQ Durch Erstellen einer neuen Kopie eines vorhandenen Objekts, um innerhalb des Silos eine alternative Zugriffsmöglichkeit darauf zu bieten QQ Durch Erstellen einer symbolischen Verknüpfung zu einem vorhandenen Objekt, um den direkten Zugriff darauf zu ermöglichen QQ Durch Erstellen eines ganz neuen Objekts, das nur innerhalb des Silos existiert, ähnlich wie die Objekte, die eine Containeranwendung nutzt Diese grundlegenden Möglichkeiten werden dann mit dem von Docker verwendeten Dienst Vmcompute (Virtual Machine Computer) kombiniert, der mit weiteren Komponenten zusammen- arbeitet, um eine vollständige Isolierschicht zu bilden: QQ Eine grundlegende WIM-Datei (Windows Image) als Basisbetriebssystem Dadurch wird eine separate Kopie des Betriebssystems bereitgestellt. Zurzeit bietet Microsoft sowohl ein Server Core- als auch ein Nano Server-Abbild an. QQ Die Bibliothek Ntdll.dll des Hostbetriebssystems Sie überschreibt die gleichnamige Bibliothek im Abbild des Basisbetriebssystems. Das liegt daran, dass Serversilos denselben Kernel und dieselben Treiber nutzen. Da Ntdll.dll Systemaufrufe verarbeitet, ist sie die einzige Benutzermoduskomponente, die das Hostbetriebssystem wiederverwenden muss. QQ Ein virtuelles Sandbox-Dateisystem, bereitgestellt vom Filtertreiber Wcifs.sys Dadurch wird es möglich, dass der Container vorübergehende Änderungen am Dateisystem vornimmt, ohne dass sich dies auf das zugrunde liegende NTFS-Laufwerk auswirkt. Nach dem Herunterfahren des Containers können diese Änderungen gelöscht werden. QQ Eine virtuelle Sandbox-Registrierung, bereitgestellt von der Kernelkomponente VReg Dadurch kann ein vorübergehender Satz von Registrierungshives bereitgestellt werden. Außerdem bildet dies eine weitere Schicht zur Namensraumisolierung, da der Stammnamensraum des Objekt-Managers nur den Registrierungsstamm isolieren kann, nicht aber die Hives. Jobs Kapitel 3 205
QQ Der Sitzungs-Manager (Smss.exe) Er wird jetzt verwendet, um zusätzliche Dienstoder Konsolensitzungen zu erstellen. Diese neue Fähigkeit wird zur Unterstützung von Con­tainern benötigt. Dazu wird Smss so erweitert, dass er nicht nur zusätzliche Benutzer­ sitzungen handhaben kann, sondern auch die Sitzungen, die von den gestarteten Con­ tainern benötigt werden. Abb. 3–16 zeigt die Architektur solcher Container und die zuvor genannten Komponenten. Hostbetriebssystem Container 1 Container 2 Grundlegendes Windows-Abbild (WIM) Grundlegendes Windows-Abbild (WIM) Silo Silo Virtuelle Registrierung Container-Manager Virtuelle Registrierung Virtuelles DS Prozess Host­ compute­ dienst Host­ netzwerk­ dienst Andere Dienste Host-SMSS Prozess Dienste Host-SMSS Host-NTDLL Virtuelles DS Prozess Dienste Host-NTDLL Host-DS Wcifs.sys VReg Ntfs.sys Konfigurations-Manager Host­regis­ trierung Exekutive und Kernel Abbildung 3–16 Containerarchitektur Silogrenzen Die genannten Komponenten bilden eine Umgebung für die Isolierung im Benutzermodus. Da jedoch die Hostkomponente Ntdll.dll verwendet wird, die mit dem Hostkernel und den Hosttreibern kommuniziert, ist es wichtig, zusätzliche Isolierungsgrenzen zu erstellen, die der Kernel anbietet, um zwischen den Silos zu unterscheiden. Dazu enthält jeder Serversilo sein eigenes, isoliertes Exemplar der folgenden Elemente: QQ Gemeinsame Silodaten (SILO_USER_SHARED_DATA) Sie enthalten den benutzerdefinierten Systempfad, die Sitzungs-ID, die Vordergrund-PID sowie Produkttyp bzw. -­suite. Dies sind Elemente der ursprünglichen KUSER_SHARED_DATA-Struktur. Sie dürfen jedoch nicht vom Host bezogen werden, da sie dort auf Informationen für das Host- statt das Basisbetriebssystemabbild verweisen, das stattdessen verwendet werden soll. Mehrere Komponenten und APIs wurden modifiziert, um beim Nachschlagen solcher Daten die gemeinsamen Silodaten statt der gemeinsamen Benutzerdaten zu lesen. Die ursprüngliche KUSER_SHARED_DATA-Struktur bleibt mit ihrer ursprünglichen Sicht der Hostinformationen an der üblichen Adresse. Daher ist dies eine Möglichkeit, durch die der Hoststatus in den Containerstatus durchsickern kann. QQ Namensraum des Objektverzeichnisstamms Er enthält seinen eigenen symbolischen \SystemRoot-Link, sein eigenes \Device-Verzeichnis (durch das alle Benutzermoduskompo- 206 Kapitel 3 Prozesse und Jobs
nenten indirekt auf die Gerätetreiber zugreifen), eigene Geräte- und DOS-Geräte-Zuordnungen (über die Benutzermodusanwendungen beispielsweise auf Treiber zugreifen, die im Netzwerk zugeordnet sind), ein eigenes \Sessions-Verzeichnis usw. QQ API-Set-Zuordnung Sie basiert auf dem API-Set-Schema des Basisbetriebssystemabbilds, also nicht auf dem Schema, das im Dateisystem des Hostbetriebssystems gespeichert ist. Wie Sie wissen, nutzt der Lader API-Set-Zuordnungen, um zu bestimmen, welche DLL eine bestimmte Funktion implementiert. Dies kann sich von einer SKU zur anderen unterscheiden. Anwendungen müssen daher die SKU des Basis- und nicht des Hostbetriebssystems erkennen können. QQ Anmeldesitzung Sie ist mit den lokal eindeutigen IDs (Local Unique ID, LUID) SYSTEM und Anonymous sowie der LUID des virtuellen Dienstkontos verbunden, das den Benutzer im Silo beschreibt. Im Grunde genommen steht die für das Token der Dienste und der Anwendung, die innerhalb der von Smss erstellten Containerdienstsitzung ausgeführt werden. Weitere Informationen über LUIDs und Anmeldesitzungen erhalten Sie in Kapitel 7. QQ ETW-Verfolgung und Loggerkontexte Sie dienen zur Isolierung von ETW-Operationen im Silo, damit die Status nicht zum Container oder Hostbetriebssystem durchsickern (siehe Kapitel 9 in Band 2). Silokontexte Neben diesen Isoliergrenzen, die der Kernelkern des Hostbetriebssystems bereitstellt, können andere Komponenten innerhalb des Kernels sowie Treiber (auch von Drittanbietern) zusätzliche Kontextdaten für Silos bereitstellen. Dazu richten Sie mit der API PsCreateSiloContext benutzerdefinierte Daten für einen Silo ein oder verknüpfen ein bestehendes Objekt mit einem Silo. Jeder Silokontext dieser Art nutzt einen Siloslotindex, der in alle laufenden und künftigen Serversilos eingefügt wird und einen Zeiger auf den Kontext enthält. Im Lieferumfang des Systems sind 32 systemweite Speicherslotindizes sowie 256 Erweiterungsslots enthalten, was sehr viele Möglichkeiten zur Erweiterung bietet. Wenn ein Serversilo erstellt wird, erhält er ein Array für seinen lokalen Silospeicher (Silo-Local Storage, SLS), der vergleichbar mit dem lokalen Threadspeicher eines Threads ist (TLS). Die Einträge in diesem Array geben die Slotindizes an, die zur Speicherung von Silokontexten zugewiesen sind. Die einzelnen Silos haben am selben Slotindex jeweils einen anderen Zeiger, speichern dort aber stets einen gleichartigen Kontext. (Beispielsweise kann dem Treiber Foo in allen Silos der Index 5 gehören, an dem er dann in jedem Silo einen anderen Zeiger bzw. Kontext speichert.) Einige dieser Slots werden auch von integrierten Kernelkomponenten wie dem Objekt-Manager, dem Security Reference Monitor (SRM) und dem Konfigurations-Manager genutzt, andere von mitgelieferten Treibern (wie Afd.sys, dem Treiber für zusätzliche WinSock-Funktionen). Wie für den Umgang mit gemeinsamen Silodaten wurden mehrere Komponenten und APIs modernisiert, um die Daten von dem entsprechenden Silokontext abzurufen statt von dem, was einmal eine globale Kernelvariable war. Beispielsweise hat jeder Container jetzt seinen eigenen Lsass.exe-Prozess, und da der SRM des Kernels ein eigenes Handle für diesen Prozess benötigt (siehe Kapitel 7), kann dies nicht mehr ein Singleton in einer globalen Varia- Jobs Kapitel 3 207
blen sein. Daher greift der SRM jetzt auf das Handle zu, indem er den Silokontext des aktiven Serversilos abfragt und die Variable dann aus der zurückgegebenen Datenstruktur entnimmt. Doch was geschieht mit dem Lsass.exe-Prozess, der auf dem Hostbetriebssystem ausgeführt wird? Wie kann der SRM auf sein Handle zugreifen, da es doch keinen Serversilo für diese Prozesse und diese Sitzung (Sitzung 0) gibt? Für diesen Zweck unterhält der Kernel jetzt einen Stammhostsilo. Anders ausgedrückt, der Host selbst wird als Teil eines Silos betrachtet. Dabei handelt es sich jedoch nicht um einen Silo in der eigentlichen Bedeutung, sondern um einen Klick, damit Kontextabfragen für den aktuellen Silo auch dann funktionieren, wenn es gar keinen aktuellen Silo gibt. Dazu wird die globale Kernelvariable PspHostSiloGlobals gespeichert, die über ein eigenes Array für den lokalen Silospeicher sowie über andere Silokontexte verfügt, die von den integrierten Kernelkomponenten verwendet werden. Bei verschiedenen Silo-APIs wird ein Aufruf mit dem NULL-Zeiger als »kein Silo – verwende den Hostsilo« gedeutet. Experiment: Den SRM-Silokontext für den Hostsilo ausgeben Auch ein Windows 10-System muss nicht unbedingt Serversilos unterhalten, insbesondere wenn es sich um ein Clientsystem handelt. Es ist jedoch stets ein Hostsilo vorhanden, der die isolierten Silokontexte für den Kernel enthält. Die Windows-Debuggererweiterung !silo können Sie zusammen mit dem Parameter -g Host verwenden, um eine Ausgabe wie die folgende zu bekommen: lkd> !silo -g Host Server silo globals fffff801b73bc580: Default Error Port: ffffb30f25b48080 ServiceSessionId : 0 Root Directory : 00007fff00000000 '' State : Running In der Ausgabe bildet der Zeiger auf die globalen Silovariablen einen Hyperlink. Wenn Sie darauf klicken, sehen Sie die folgende Befehlsausführung als Ausgabe: lkd> dx -r1 (*((nt!_ESERVERSILO_GLOBALS *)0xfffff801b73bc580)) (*((nt!_ESERVERSILO_GLOBALS *)0xfffff801b73bc580)) [Type: _ESERVERSILO_GLOBALS] [+0x000] ObSiloState [Type: _OBP_SILODRIVERSTATE] [+0x2e0] SeSiloState [Type: _SEP_SILOSTATE] [+0x310] SeRmSiloState [Type: _SEP_RM_LSA_CONNECTION_STATE] [+0x360] CmSiloState : 0xffffc308870931b0 [Type: _CMP_SILO_CONTEXT *] [+0x368] EtwSiloState : 0xffffb30f236c4000 [Type: _ETW_SILODRIVERSTATE *] ... Wenn Sie jetzt auf das Feld SeRmSiloState klicken, wird es erweitert und zeigt unter anderem einen Zeiger auf den Prozess Lsass.exe: lkd> dx -r1 ((ntkrnlmp!_SEP_RM_LSA_CONNECTION_STATE *)0xfffff801b73bc890) ((ntkrnlmp!_SEP_RM_LSA_CONNECTION_STATE *)0xfffff801b73bc890) : → 208 Kapitel 3 Prozesse und Jobs
0xfffff801b73bc890 [Type: _SEP_RM_LSA_CONNECTION_STATE *] [+0x000] LsaProcessHandle : 0xffffffff80000870 [Type: void *] [+0x008] LsaCommandPortHandle : 0xffffffff8000087c [Type: void *] [+0x010] SepRmThreadHandle : 0x0 [Type: void *] [+0x018] RmCommandPortHandle : 0xffffffff80000874 [Type: void *] Siloüberwachung Kerneltreiber können sich mit ihrem eigenen Silokontext versehen. Woher aber wissen sie, welche Silos ausgeführt werden und welche neuen Silos als Container erstellt und gestartet werden? Dazu ist die Siloüberwachung da. Sie stellt APIs für Benachrichtigungen darüber bereit, dass ein Serversilo erstellt oder beendet wurde (PsRegisterSiloMonitor, PsStartSiloMonitor, PsUnregisterSiloMonitor), sowie über jegliche bereits existierenden Silos. Jede Siloüberwachung kann durch den Aufruf von PsGetSiloMonitorContextSlot ihren eigenen Slotindex abrufen und dann nach Bedarf in den Funktionen PsInsertSiloContext, PsReplaceSiloContext und PsRemoveSiloContext nutzen. Mit PsAllocSiloContextSlot lassen sich auch weitere Slots zuweisen, was allerdings nur dann erforderlich ist, wenn eine Komponente aus irgendeinem Grund zwei Kontexte speichern möchte. Treiber können auch die APIs PsInsertPermanentSiloContext oder PsMakeSiloContextPermanent nutzen, um dauerhafte Silokontexte anzulegen, bei denen keine Referenzzählung stattfindet und die nicht an die Lebensdauer des Serversilos oder die Anzahl der Get-Funktionen für Silokontexte gebunden sind. Ein solcher dauerhafter Kontext kann mit PsGetSiloContext und PsGetPermanentSiloContext abgerufen werden. Experiment: Siloüberwachung und -kontext Damit Sie ein Bild von der Verwendung der Siloüberwachung und der Speicherung von Silokontexten bekommen, schauen wir uns den Treiber für zusätzliche WinSock-Funktionen (Afd.sys) und die zugehörige Überwachung an. Als Erstes geben wir die Datenstruktur für die Überwachung aus. Leider befindet sie sich nicht in den Symboldateien, weshalb wir dies in Form von Rohdaten tun müssen. lkd> dps poi(afd!AfdPodMonitor) ffffe387'a79fc120 ffffe387'a7d760c0 ffffe387'a79fc128 ffffe387'a7b54b60 ffffe387'a79fc130 00000009'00000101 ffffe387'a79fc138 fffff807'be4b5b10 afd!AfdPodSiloCreateCallback ffffe387'a79fc140 fffff807'be4bee40 afd!AfdPodSiloTerminateCallback Als Nächstes rufen wir den Slot (in diesem Beispiel 9) vom Hostsilo ab. Silos speichern ihren SLS im Feld Storage. Es umfasst ein Array aus Datenstrukturen (Sloteinträgen), die wiederum jeweils einen Zeiger enthalten, sowie einige Flags. Um den Offset für den richtigen Sloteintrag zu bekommen, multiplizieren wir den Index mit 2. Anschließend greifen wir auf das zweite Feld (+1) zu, damit wir den Zeiger auf den Kontextzeiger erhalten: → Jobs Kapitel 3 209
lkd> r? @$t0 = (nt!_ESERVERSILO_GLOBALS*)@@masm(nt!PspHostSiloGlobals) lkd> ?? ((void***)@$t0->Storage)[9 * 2 + 1] void ** 0xffff988f'ab815941 Das Permanenzflag (0x2) wird mit einer OR-Verknüpfung in den Zeiger eingeschlossen. Maskieren Sie es und vergewissern Sie sich dann mit dem Ausdruck !object, dass das Ergebnis wirklich ein Silokontext ist: lkd> !object (0xffff988f'ab815941 & -2) Object: ffff988fab815940 Type: (ffff988faaac9f20) PsSiloContextNonPaged Serversilos erstellen Da Silos, wie bereits erwähnt, ein Merkmal von Jobobjekten sind, müssen Sie zum Erstellen eines Serversilos zunächst ein Jobobjekt anlegen. Dazu verwenden Sie die API CreateJobObject, die im Rahmen des Anniversary-Updates so modifiziert wurde, dass Jobs jetzt eine Job-ID (JID) haben. Sie stammt aus demselben Wertebereich wie die Prozess- und die Thread-ID, nämlich aus der Client-ID-Tabelle. Dadurch ist die JID nicht nur unter den anderen Jobs, sondern auch unter den Prozessen und Threads einzigartig. Außerdem wird automatisch ein Container-GUID erstellt. Anschließend wird die API SetInformationJobObject mit der Informationsklasse Siloerstellung verwendet. Dadurch wird im EJOB-Exekutivobjekt für den Job das Flag Silo gesetzt und das SLS-Slotarray im Element Storage zugewiesen. Damit liegt jetzt ein Anwendungssilo vor. Danach wird mit einem weiteren Aufruf von SetInformationJobObject, aber einer anderen Informationsklasse, der Namensraum für das Stammobjektverzeichnis erstellt. Für die neue Klasse ist das Recht Trusted Computing Base (TCB) erforderlich. Da Silos normalerweise nur vom Dienst Vmcompute erstellt werden, wird dadurch sichergestellt, dass virtuelle Objektnamensräume nicht böswillig verwendet werden, um Anwendungen zu verwirren oder sogar funktionsunfähig zu machen. Beim Anlegen des Namensraums erstellt oder öffnet der Objekt-­ Manager ein neues Siloverzeichnis unter dem echten Hoststamm (\) und hängt die JID an, um einen neuen virtuellen Stamm zu bilden (z. B. \Silos\148\). Anschließend erstellt er die Objekte KernelObjects, ObjectTypes, GLOBALROOT und DosDevices. Der Stamm wird dann als Silokontext mit dem Slotindex gespeichert, der sich in PsObjectDirectorySiloContextSlot befindet und vom Objekt-Manager beim Start zugewiesen wurde. Der nächste Schritt besteht darin, den Silo zu einem Serversilo zu machen. Dazu wird wiederum SetInformationJobObject mit einer weiteren Informationsklasse aufgerufen. Im Kernel wird die Funktion PspConvertSiloToServerSilo ausgeführt, die die ESERVERSILO_GLOBALS-Struktur initialisiert (die Sie schon in dem Experiment gesehen haben, in dem Sie PspHostSiloGlobals mit dem Befehl !silo ausgegeben haben). Dadurch werden die gemeinsamen Silodaten, die API-Set-Zuordnung, der Systemstamm und diverse Silokontexte initialisiert, z. B. derjenige, mit dem die SRM den Lsass.exe-Prozess erkennt. Während des Konvertierungsvorgangs erhalten die Siloüberwachungen, die Callbacks registriert und gestartet haben, entsprechende Nachrichten, z. B. dass sie ihre eigenen Silokontextdaten hinzufügen können. 210 Kapitel 3 Prozesse und Jobs
Der letzte Schritt besteht darin, den Serversilo durch Initialisieren einer neuen Dienstsitzung zu starten, also praktisch Sitzung 0 für den Serversilo. Dies erfolgt durch eine ALPC-Nachricht, die an den Smss-Port SmApiPort gesendet wird und ein Handle für das von Vmcompute erstellte und nun zu einem Serversilo umgewandelte Jobobjekt enthält. Wie beim Erstellen einer normaler Benutzersitzung klont sich Smss selbst, wobei der Klon diesmal jedoch mit dem Jobobjekt verbunden wird. Dadurch wird die neue Smss-Kopie an Containerelemente des Serversilos angehängt. Smss glaubt, dass dies Sitzung 0 ist, und führt seine üblichen Pflichten aus, z. B. den Start von Scrss.exe, Wininit.exe, Lsass.exe usw. Dieser »Startprozess« fährt wie gewöhnlich fort, wobei Wininit.exe als Nächstes den Dienststeuerungsmanager (Services.exe) startet, der wiederum alle Dienste mit automatischem Start in Gang setzt usw. Im Serversilo können jetzt auch neue Anwendungen ausgeführt werden. Wie zuvor beschrieben, werden sie mit einer Anmeldesitzung ausgeführt, die mit einem LUID für ein virtuelles Dienstkonto verbunden ist. Hilfsfunktionen Vielleicht ist Ihnen aufgefallen, dass ein Startprozess wie in der vorstehenden kurzen Beschreibung nicht erfolgreich sein kann. Beispielsweise würde er im Rahmen der Initialisierung versuchen, die benannte Pipe ntsvcs zu erstellen, für die eine Kommunikation mit \Device\NamedPipe bzw. aus der Sicht von Services.exe mit \Silos\JID\Device\NamedPipe erforderlich wäre. Ein solches Geräteobjekt gibt es aber nicht! Damit Gerätetreiber auf Funktionen zugreifen können, müssen sie daher Informationen bekommen und ihre eigenen Siloüberwachungen registrieren, die dann Benachrichtigungen nutzen, um ihre eigenen Geräteobjekte für die einzelnen Silos zu erstellen. Der Kernel bietet die API PsAttachSiloToCurrentThread (und das Pendant PsDetachSiloFromCurrentThread), die vorübergehend das Feld Silo in der ETHREAD-Struktur des übergebenen Jobobjekts ausfüllt. Dadurch wird jeglicher Zugriff, z. B. auf den Objekt-Manager, so behandelt, als käme er von dem Silo. Beispielsweise kann der Treiber für benannte Pipes diesen Mechanismus nutzen, um ein NamedPipe-Objekt im Namensraum \Device zu erstellen, der jetzt zu \Silos\JID\ gehört. Des Weiteren stellt sich die Frage, wie Anwendungen, die im Grunde genommen in einer »Dienstsitzung« gestartet werden, interaktiv sein und Ein- und Ausgaben verarbeiten können. Beim Start in einem Windows-Container sind weder grafische Benutzeroberflächen noch die Verwendung eines Remotedesktops für den Zugriff auf einen Container möglich. Daher können nur Befehlszeilenanwendungen ausgeführt werden. Doch auch sie brauchen gewöhnlich eine interaktive Sitzung. Wie also können sie funktionieren? Das Geheimnis liegt in dem besonderen Hostprozess CExecSvc.exe, der den Containerausführungsdienst implementiert. Dieser Dienst verwendet eine benannte Pipe zur Kommunikation mit dem Docker- und dem Vmcompute-Dienst auf dem Host und startet die eigentlichen Containeranwendungen in der Sitzung. Er emuliert auch die Konsolenfunktion, die normalerweise von Conhost.exe bereitgestellt wird, indem er die Ein- und Ausgabe durch eine benannte Pipe zum Fenster der eigentlichen Eingabeaufforderung (oder PowerShell) leitet, das ursprünglich zur Ausführung des Befehls docker auf dem Host verwendet wurde. Dieser Dienst wird auch für Befehle wie docker cp zur Übertragung von Dateien vom oder zum Container verwendet. Jobs Kapitel 3 211
Die Containervorlage Neben den Geräteobjekten, die beim Erstellen eines Silos von den Treibern angelegt werden, gibt es noch zahllose weitere Objekte, die vom Kernel und anderen Komponenten gebildet werden. Die Dienste, die in Sitzung 0 laufen, erwarten, mit all diesen Objekten zu kommunizieren, und umgekehrt. Im Benutzermodus gibt es kein Siloüberwachungssystem, das diesen Bedarf befriedigen könnte. Es wäre auch nicht sinnvoll, jeden Treiber zu zwingen, immer ein besonderes Geräteobjekt zur Darstellung jedes Silos zu erstellen. Wenn ein Silo Musik auf der Soundkarte abspielen möchte, sollte er kein separates Geräteobjekt zur Darstellung derselben Karte verwenden müssen, die auch von allen anderen Silos und dem Host selbst verwendet wird. Das wäre nur dann erforderlich, wenn eine Klangisolierung zwischen den einzelnen Silos nötig wäre. Ein weiteres Beispiel ist der AFD. Er verwendet zwar eine Siloüberwachung, mit der er aber nur ermittelt, welcher Benutzermodusdienst den DNS-Client beherbergt, mit dem der AFD sprechen muss, um Kernelmodus-DNS-Anforderungen zu erfüllen. Diese Information gilt jeweils für einen Silo. Die Überwachung dient nicht dazu, separate \Silos\JID\Device\Afd-Objekte zu erstellen, da es in dem System nur einen einzigen Netzwerk-/WinSock-Stack gibt. Neben Treibern und Objekten enthält die Registrierung auch verschiedene globale Informationen, die in allen Silos vorhanden und sichtbar sein müssen. Um diese Informationen herum kann dann die Komponente VReg eine Sandbox einrichten. Um all diese Bedürfnisse zu erfüllen, werden der Silonamensraum, die Registrierung und das Dateisystem in einer besonderen Containervorlagendatei definiert, die sich standardmäßig in %SystemRoot%\System32\Containers\wsc.def befindet, wenn das Merkmal Windows-Con­ tainer im Dialogfeld Features hinzufügen/entfernen aktiviert wurde. Diese Datei beschreibt den Namensraum für den Objekt-Manager und die Registrierung und die zugehörigen Regeln, was die bedarfsweise Definition von symbolischen Verknüpfungen zu den echten Objekten auf dem Host ermöglicht. Außerdem gibt sie an, welches Jobobjekt, welche Volumebereitstellungspunkte und welche Netzwerkisolierungsrichtlinien verwendet werden sollen. Theoretisch kann in zukünftigen Windows-Versionen auch die Verwendung anderer Vorlagendateien zugelassen sein, um andere Arten von Containerumgebungen möglich zu machen. Das folgende Listing zeigt einen Auszug aus Wsc.def auf einem System mit aktivierten Containern: <!-- Silodefinitionsdatei für Cmdserver.exe --> <container> <namespace> <ob shadow="false"> <symlink name="FileSystem" path="\FileSystem" scope="Global" /> <symlink name="PdcPort" path="\PdcPort" scope="Global" /> <symlink name="SeRmCommandPort" path="\SeRmCommandPort" scope="Global" /> <symlink name="Registry" path="\Registry" scope="Global" /> <symlink name="Driver" path="\Driver" scope="Global" /> <objdir name="BaseNamedObjects" clonesd="\BaseNamedObjects" shadow="false"/> <objdir name="GLOBAL??" clonesd="\GLOBAL??" shadow="false"> 212 Kapitel 3 Prozesse und Jobs
<!-- Verzeichnisse müssen vom Host zugeordnet werden --> <symlink name="ContainerMappedDirectories" path="\ ContainerMappedDirectories" scope="Local" /> <!-- Gültige Links zu \Device --> <symlink name="WMIDataDevice" path="\Device\WMIDataDevice" scope="Local" /> <symlink name="UNC" path="\Device\Mup" scope="Local" /> ... </objdir> <objdir name="Device" clonesd="\Device" shadow="false"> <symlink name="Afd" path="\Device\Afd" scope="Global" /> <symlink name="ahcache" path="\Device\ahcache" scope="Global" /> <symlink name="CNG" path="\Device\CNG" scope="Global" /> <symlink name="ConDrv" path="\Device\ConDrv" scope="Global" /> ... <registry> <load key="$SiloHivesRoot$\Silo$TopLayerName$Software_Base" path="$TopLayerPath$\Hives\Software_Base" ReadOnly="true" /> ... <mkkey name="ControlSet001" clonesd="\REGISTRY\Machine\SYSTEM\ControlSet001" /> <mkkey name="ControlSet001\Control" clonesd="\REGISTRY\Machine\SYSTEM\ControlSet001\Control" /> Zusammenfassung Dieses Kapitel hat die Struktur von Prozessen, ihre Erstellung und Zerstörung gezeigt. Wir haben uns angesehen, wie Jobs verwendet werden können, um eine Gruppe von Prozessen als eine Einheit zu verwalten, und wie Serversilos eine neue Ära der Containerunterstützung in Windows Server-Versionen einläuten. Im nächsten Kapitel geht es um Threads – um ihre Struktur und ihre Operation, ihre Einplanung zur Ausführung und die verschiedenen Möglichkeiten, um sie zu bearbeiten und zu nutzen. Zusammenfassung Kapitel 3 213

K apite l 4 Threads Dieses Kapitel erklärt die Datenstrukturen und Algorithmen im Zusammenhang mit Threads und der Threadplanung in Windows. Im ersten Abschnitt geht es darum, wie Threads erstellt werden. Danach werden die internen Mechanismen von Threads und der Threadplanung beschrieben. Abgeschlossen wird das Kapitel mit einer Erörterung von Threadpools. Threads erstellen Bevor wir uns mit den internen Strukturen zur Verwaltung von Threads beschäftigen, wollen wir uns ansehen, wie Threads erstellt werden und welche Schritte und Argumente dazu erforderlich sind. Die einfachste Möglichkeit, um im Benutzermodus einen Thread zu erstellen, bietet die Funktion CreateThread. Sie legt einen Thread im aktuellen Prozess an und nimmt die folgenden Argumente entgegen: QQ Eine optionale Struktur mit Sicherheitsattributen Sie nennt den Sicherheitsdeskriptor, der mit dem neuen Thread verbunden werden soll, und gibt an, ob das Threadhandle vererbbar sein soll. (Die Handlevererbung wird in Kapitel 8 von Band 2 behandelt.) QQ Eine optionale Stackgröße Wird hier 0 angegeben, so wird der Standardwert aus dem Header der ausführbaren Datei verwendet. Dies gilt stets für den ersten Thread in einem Benutzermodusprozess. (Der Threadstack wird in Kapitel 5 ausführlicher beschrieben.) QQ Ein Funktionszeiger Er dient als Eintrittspunkt für die Ausführung des neuen Threads. QQ Ein optionales Argument übergeben. Dies dient dazu, Argumente an die Funktion des Threads zu QQ Zwei optionale Flags Eines dieser Flags gibt an, ob der Thread im angehaltenen Zustand gestartet wird (CREATE_SUSPENDED). Das andere steuert die Interpretation des Stackgrößenarguments (ursprüngliche zugesicherte Größe oder maximale reservierte Größe). Nach erfolgreichem Abschluss werden ein von null verschiedenes Handle für den neuen Thread und, falls vom Aufrufer angefordert, eine eindeutige Thread-ID zurückgegeben. Mit CreateRemoteThread steht eine erweiterte Funktion zur Threaderstellung zur Verfügung. Sie nimmt als zusätzliches erstes Argument ein Handle für den Zielprozess entgegen, in dem der Thread erstellt wird. Damit können Sie einen Thread in einen anderen Prozess injizieren. Diese Technik wird häufig in Debuggern genutzt, um ein Anhalten in einem zu debuggenden Prozess zu erzwingen. Dabei injiziert der Debugger den Thread, der sofort die Funktion DebugBreak aufruft und damit einen Haltepunkt einrichtet. Eine weitere gängige Anwendung dieser Technik besteht darin, einen Prozess interne Informationen eines anderen Prozesses gewinnen 215
zu lassen. Das geht leichter, wenn der Thread im Kontext des Zielprozesses ausgeführt wird (unter anderem ist dabei der gesamte Adressraum sichtbar). Dies kann sowohl für legitime als auch für schädliche Zwecke eingesetzt werden. Damit CreateRemoteThread funktioniert, muss das Prozesshandle mit ausreichend Zugriffsrechten für eine solche Operation versehen sein. Eine Injektion in geschützte Prozesse ist damit nicht möglich, da Handles für solche Prozesse nur stark eingeschränkte Rechte haben. Als letzte Funktion wollen wir hier noch CreateRemoteThreadEx erwähnen, die den beiden zuvor erwähnten übergeordnet ist. Die Implementierungen von CreateThread und Create­ RemoteThread rufen einfach CreateRemoteThreadEx mit den entsprechenden Standardwerten auf. CreateRemoteThreadEx bietet darüber hinaus die Möglichkeit, eine Attributliste anzugeben (ähnlich wie bei der STARTUPINFOEX-Struktur beim Erstellen von Prozessen, die gegenüber der STARTUPINFO-Struktur ein zusätzliches Element enthält). Zu diesen Attributen gehören beispielsweise Einstellungen für den idealen Prozessor und die Gruppenaffinität (die beide weiter hinten in diesem Kapitel behandelt werden). Wenn alles gut geht, ruft CreateRemoteThreadEx schließlich NtCreateThreadEx in Ntdll.dll auf. Dabei erfolgt der übliche Übergang in den Kernelmodus, wobei die Ausführung in der Exekutivfunktion NtCreateThreadEx fortgesetzt wird. Die Kernelmodusaspekte der Threaderstellung laufen dort ab (siehe weiter hinten in diesem Kapitel im Abschnitt »Geburt eines Threads«). Die Threaderstellung im Kernelmodus wird durch die Funktion PsCreateSystemThread erreicht (die im WDK dokumentiert ist). Das ist praktisch für Treiber, die als unabhängige Prozesse innerhalb des Systemprozesses laufen müssen (das heißt, sie sind nicht mit einem bestimmten Prozess verbunden). Die Funktion kann einen Thread auch unter einem beliebigen Prozess erstellen, was für Treiber jedoch nicht von Bedeutung ist. Bei der Beendigung der Funktion eines Kernelthreads wird das Threadobjekt nicht automatisch zerstört. Stattdessen müssen die Treiber PsTerminateSystemThread aus der Threadfunktion heraus aufrufen, um den Thread ordnungsgemäß zu beenden. Diese Funktion springt logischerweise nie zurück. Interne Strukturen von Threads In diesem Abschnitt geht es um die internen Strukturen, die im Kernel (und manchmal auch im Benutzermodus) verwendet werden, um Threads zu verwalten. Sofern nicht ausdrücklich anders angegeben, gilt alles, was in diesem Abschnitt beschrieben wird, sowohl für Benutzer- als auch für Kernelmodusthreads. Datenstrukturen Auf Betriebssystemebene wird ein Windows-Thread als ein Exekutivthreadobjekt dargestellt. Es schließt eine ETHREAD-Struktur (siehe Abb. 4–1) ein, die wiederum eine KTHREAD-Struktur (siehe Abb. 4–2) als erstes Element enthält. Die ETHREAD-Struktur und die Strukturen, auf die sie zeigt, befinden sich im Systemadressraum. Die einzige Ausnahme bildet der Threadumgebungsblock (TEB), der im Prozessadressraum steht (ähnlich wie der PEB, da die Benutzermoduskomponenten darauf zugreifen müssen). 216 Kapitel 4 Threads
Threadsteuerungsblock (TCB) Erstellungs- und Beendigungszeit des Threads ID des Threads und des Elternprozesses Prozess, zu dem der Thread gehört Threadflags Zugriffstoken Threadstartadresse Informationen zum Identitätswechsel Ausstehende E/A-Anforderungen Timerinformationen ALPC-Nachrichteninformationen Threadlisteneintrag Threadsperre Beendigungscode Energieverbrauchswerte (Win 10) Informationen zum CPU-Satz (Win 10) Abbildung 4–1 Wichtige Felder der Exekutivthreadstruktur (ETHREAD) Dispatcher-Header Threadumgebungsblock (TEB) Kernel- und Benutzerzeit Zeiger auf die Systemdiensttabelle (x86) Systemdiensttabelle Einfrier- und Unterbrechungszähler Win32-Threadobjekt Elternprozess Stackinformationen Planungsinformationen Trap-frame Synchronisierungsinformationen Warteblock Liste der Objekte, auf die der Thread wartet Liste der ausstehenden APCs Threadlisteneintrag Abbildung 4–2 Wichtige Felder der Kernelthreadstruktur (KTHREAD) Der Windows-Teilsystemprozess Csrss unterhält für jeden Thread, der in einer Windows-Teilsystemanwendung erstellt wurde, eine parallele Struktur namens CSR_THREAD. Für jeden Thread, der eine USER- oder GDI-Funktion des Windows-Teilsystems aufgerufen hat, führt der Kernelmodusteil des Windows-Teilsystems (Win32k.sys) auch eine eigene Datenstruktur (W32THREAD), auf die die KTHREAD-Struktur zeigt. Interne Strukturen von Threads Kapitel 4 217
Die Win32k-Threadstruktur gehört zur Exekutive, ist eine Struktur höherer Ebene und steht im Zusammenhang mit der Grafikdarstellung. Dass sich der Zeiger da­ rauf in der KTHREAD- und nicht in der ETHREAD-Struktur befindet, scheint Schichtverletzung oder ein Versehen in der Abstraktionsarchitektur des Standardkernels zu sein. Der Scheduler und andere maschinennahe Komponenten verwenden dieses Feld nicht. Die meisten Felder in Abb. 4–1 sind selbsterklärend. Das erste Element der ETHREAD-Struktur ist der Threadsteuerungsblock (TCB), bei dem es sich um eine Struktur vom Typ KTHREAD handelt. Auf ihn folgen Angaben zur Bezeichnung des Threads und des Prozesses (unter anderem ein Zeiger auf den Besitzerprozess, sodass Zugriff auf dessen Umgebungsinformationen besteht), Sicherheitsinformationen in Form eines Zeigers auf das Zugriffstoken und Angaben für Identitätswechsel, Felder für ALPC-Nachrichten (Advanced Local Procedure Calls) und ausstehende E/A-Anforderungen (IRPs) sowie Windows 10-spezifische Felder zur Energieverwaltung (siehe Kapitel 6) und zu CPU-Sätzen (weiter hinten in diesem Kapitel erklärt). Einige dieser Felder werden in diesem Buch noch ausführlicher erläutert. Um sich den Aufbau einer ETHREAD-Struktur genau anzusehen, können Sie mit dem Kerneldebuggerbefehl dt ihr Format anzeigen lassen. Sehen wir uns nun zwei der bereits erwähnten wichtigen Threaddatenstrukturen genauer an, nämlich die ETHREAD- und die KTHREAD-Struktur. Die KTHREAD-Struktur ist das Element Tcb der ETHREAD-Struktur und enthält Informationen, die der Windows-Kernel benötigt, um Threadplanungs-, Synchronisierungs- und Zeiteinhaltungsfunktionen auszuführen. Experiment: ETHREAD- und KTHREAD-Strukturen anzeigen Die ETHREAD- und KTHREAD-Strukturen können Sie mit dem Befehl dt des Kerneldebuggers einsehen. Die folgende Ausgabe zeigt das Format einer ETHREAD-Struktur auf einem 64-Bit-System mit Windows 10: lkd> dt nt!_ethread +0x000 Tcb : _KTHREAD +0x5d8 CreateTime : _LARGE_INTEGER +0x5e0 ExitTime : _LARGE_INTEGER ... +0x7a0 EnergyValues : Ptr64 _THREAD_ENERGY_VALUES +0x7a8 CmCellReferences : Uint4B +0x7b0 SelectedCpuSets : Uint8B +0x7b0 SelectedCpuSetsIndirect : Ptr64 Uint8B +0x7b8 Silo : Ptr64 _EJOB Die KTHREAD-Struktur können Sie mit einem ähnlichen Befehl ausgeben lassen. Sie können jedoch auch wie im Experiment »Das Format einer EPROCESS-Struktur anzeigen« aus Kapitel 3 den Befehl dt nt!_ETHREAD Tcb verwenden: → 218 Kapitel 4 Threads
lkd> dt nt!_kthread +0x000 Header : _DISPATCHER_HEADER +0x018 SListFaultAddress : Ptr64 Void +0x020 QuantumTarget : Uint8B +0x028 InitialStack : Ptr64 Void +0x030 StackLimit : Ptr64 Void +0x038 StackBase : Ptr64 Void +0x040 ThreadLock : Uint8B +0x048 CycleTime : Uint8B +0x050 CurrentRunTime : Uint4B ... +0x5a0 ReadOperationCount : Int8B +0x5a8 WriteOperationCount : Int8B +0x5b0 OtherOperationCount : Int8B +0x5b8 ReadTransferCount : Int8B +0x5c0 WriteTransferCount : Int8B +0x5c8 OtherTransferCount : Int8B +0x5d0 QueuedScb : Ptr64 _KSCB Experiment: Der Befehl !thread des Kerneldebuggers Der Befehl !thread des Kerneldebuggers gibt einen Teil der Informationen in den Threaddatenstrukturen aus. Einige davon können von keinem anderen Programm angezeigt werden, darunter die folgenden: QQ Adressen von internen Strukturen QQ Angaben zur Priorität QQ Informationen über den Stack QQ Liste der ausstehenden E/A-Anforderungen QQ Liste der Objekte, auf die ein Thread wartet Um Threadinformationen einzusehen, verwenden Sie den Befehl !process, der zuerst Informationen über den Prozess und dann über alle darin enthaltenen Threads ausgibt, oder den Befehl !thread mit der Adresse eines Threadobjekts, wenn Sie nur einen bestimmten Thread anzeigen lassen wollen. Im folgenden Beispiel sehen wir uns zunächst alle Instanzen von Explorer.exe an: lkd> !process 0 0 explorer.exe PROCESS ffffe00017f3e7c0 SessionId: 1 Cid: 0b7c Peb: 00291000 ParentCid: 0c34 DirBase: 19b264000 ObjectTable: ffffc00007268cc0 HandleCount: 2248. Image: explorer.exe → Interne Strukturen von Threads Kapitel 4 219
PROCESS ffffe00018c817c0 SessionId: 1 Cid: 23b0 Peb: 00256000 ParentCid: 03f0 DirBase: 2d4010000 ObjectTable: ffffc0001aef0480 HandleCount: 2208. Image: explorer.exe Anschließend wählen wir eine dieser Instanzen aus und lassen deren Threads anzeigen: lkd> !process ffffe00018c817c0 2 PROCESS ffffe00018c817c0 SessionId: 1 Cid: 23b0 Peb: 00256000 ParentCid: 03f0 DirBase: 2d4010000 ObjectTable: ffffc0001aef0480 HandleCount: 2232. Image: explorer.exe THREAD ffffe0001ac3c080 Cid 23b0.2b88 Teb: 0000000000257000 Win32Thread: ffffe0001570ca20 WAIT: (UserRequest) UserMode Non-Alertable ffffe0001b6eb470 SynchronizationEvent THREAD ffffe0001af10800 Cid 23b0.2f40 Teb: 0000000000265000 Win32Thread: ffffe000156688a0 WAIT: (UserRequest) UserMode Non-Alertable ffffe000172ad4f0 SynchronizationEvent ffffe0001ac26420 SynchronizationEvent THREAD ffffe0001b69a080 Cid 23b0.2f4c Teb: 0000000000267000 Win32Thread: ffffe000192c5350 WAIT: (UserRequest) UserMode Non-Alertable ffffe00018d83c00 SynchronizationEvent ffffe0001552ff40 SynchronizationEvent ... THREAD ffffe00023422080 Cid 23b0.3d8c Teb: 00000000003cf000 Win32Thread: ffffe0001eccd790 WAIT: (WrQueue) UserMode Alertable ffffe0001aec9080 QueueObject THREAD ffffe00023f23080 Cid 23b0.3af8 Teb: 00000000003d1000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable ffffe0001aec9080 QueueObject THREAD ffffe000230bf800 Cid 23b0.2d6c Teb: 00000000003d3000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable ffffe0001aec9080 QueueObject THREAD ffffe0001f0b5800 Cid 23b0.3398 Teb: 00000000003e3000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable ffffe0001d19d790 SynchronizationEvent ffffe00022b42660 SynchronizationTimer Die Threadliste ist hier aus Platzgründen gekürzt. Für jeden Thread wird die Adresse (ETHREAD) angezeigt, die an den Befehl !thread übergeben werden kann; die Client-ID (Cid), also die Prozess- und Thread-ID (wobei die Prozess-ID bei allen hier gezeigten Threads identisch ist, da sie alle zum selben Explorer.exe-Prozess gehören); der Threadumgebungsblock (TEB; Erklärung folgt in Kürze); und der Threadstatus (meistens Wait, wobei der Grund für den Wartevorgang in Klammern angegeben ist). In der anschließenden Zeile können die Synchronisierungsobjekte angezeigt werden, auf die der Thread wartet. → 220 Kapitel 4 Threads
Um mehr Informationen über einen bestimmten Thread zu gewinnen, übergeben Sie mit dem Befehl !thread seine Adresse: lkd> !thread ffffe0001d45d800 THREAD ffffe0001d45d800 Cid 23b0.452c Teb: 000000000026d000 Win32Thread: ffffe0001aace630 WAIT: (UserRequest) UserMode Non-Alertable ffffe00023678350 NotificationEvent ffffe00022aeb370 Semaphore Limit 0xffff ffffe000225645b0 SynchronizationEvent Not impersonating DeviceMap ffffc00004f7ddb0 Owning Process ffffe00018c817c0 Image: explorer.exe Attached Process N/A Image: N/A Wait Start TickCount 7233205 Ticks: 270 (0:00:00:04.218) Context Switch Count 6570 IdealProcessor: 7 UserTime 00:00:00.078 KernelTime 00:00:00.046 Win32 Start Address 0c Stack Init ffffd000271d4c90 Current ffffd000271d3f80 Base ffffd000271d5000 Limit ffffd000271cf000 Call 0000000000000000 Priority 9 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5 GetContextState failed, 0x80004001 Unable to get current machine context, HRESULT 0x80004001 Child-SP RetAddr : Args to Child : Call Site ffffd000'271d3fc0 fffff803'bef086ca : 00000000'00000000 00000000'00000001 00000000'00000000 00000000'00000000 : nt!KiSwapContext+0x76 ffffd000'271d4100 fffff803'bef08159 : ffffe000'1d45d800 fffff803'00000000 ffffe000'1aec9080 00000000'0000000f : nt!KiSwapThread+0x15a ffffd000'271d41b0 fffff803'bef09cfe : 00000000'00000000 00000000'00000000 ffffe000'0000000f 00000000'00000003 : nt!KiCommitThreadWait+0x149 ffffd000'271d4240 fffff803'bf2a445d : ffffd000'00000003 ffffd000'271d43c0 00000000'00000000 fffff960'00000006 : nt!KeWaitForMultipleObjects+0x24e ffffd000'271d4300 fffff803'bf2fa246 : fffff803'bf1a6b40 ffffd000'271d4810 ffffd000'271d4858 ffffe000'20aeca60 : nt!ObWaitForMultipleObjects+0x2bd ffffd000'271d4810 fffff803'befdefa3 : 00000000'00000fa0 fffff803'bef02aad ffffe000'1d45d800 00000000'1e22f198 : nt!NtWaitForMultipleObjects+0xf6 ffffd000'271d4a90 00007ffe'f42b5c24 : 00000000'00000000 00000000'00000000 00000000'00000000 00000000'00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffd000'271d4b00) 00000000'1e22f178 00000000'00000000 : 00000000'00000000 00000000'00000000 00000000'00000000 00000000'00000000 : 0x00007ffe'f42b5c24 Es wird eine Menge an Informationen über den Thread angezeigt, darunter seine Priorität, Angaben zum Stack, Benutzer- und Kernelzeiten usw. Viele dieser Informationen werden Sie in diesem Kapitel sowie in Kapitel 5 und 6 noch näher betrachten. Interne Strukturen von Threads Kapitel 4 221
Experiment: Threadinformationen mit tlist einsehen Das folgende Listing ist die ausführliche Anzeige eines Prozesses, wie sie tlist in den Debugging Tools for Windows ausgibt. (Achten Sie darauf, die passende tlist-Version für die Bitversion des Zielprozesses zu verwenden.) Hier wird in der Threadliste Win32StartAddr angezeigt, was die Adresse ist, die die Anwendung an die Funktion CreateThread übergeben hat. Alle anderen Programme (außer Process Explorer) zeigen als Threadstartadresse jedoch wirklich die tatsächliche Adresse an (eine Funktion in Ntdll.dll) und nicht die von der Anwendung angegebene. Im Folgenden sehen Sie die gekürzte Ausgabe von tlist für Word 2016: C:\Dbg\x86>tlist winword 120 WINWORD.EXE Chapter04.docm - Word CWD: C:\Users\pavely\Documents\ CmdLine: "C:\Program Files (x86)\Microsoft Office\Root\Office16\WINWORD. EXE" /n "D:\OneDrive\WindowsInternalsBook\7thEdition\Chapter04\Chapter04.docm VirtualSize: 778012 KB PeakVirtualSize: 832680 KB WorkingSetSize:185336 KB PeakWorkingSetSize:227144 KB NumberOfThreads: 45 12132 Win32StartAddr:0x00921000 LastErr:0x00000000 State:Waiting 15540 Win32StartAddr:0x6cc2fdd8 LastErr:0x00000000 State:Waiting 7096 Win32StartAddr:0x6cc3c6b2 LastErr:0x00000006 State:Waiting 17696 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 17492 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 4052 Win32StartAddr:0x70aa5cf7 LastErr:0x00000000 State:Waiting 14096 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 6220 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 7204 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 1196 Win32StartAddr:0x6ea016c0 LastErr:0x00000057 State:Waiting 8848 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 3352 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 11612 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 17420 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 13612 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 15052 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting ... 12080 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 9456 Win32StartAddr:0x77c1c6d0 LastErr:0x00002f94 State:Waiting 9808 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 16208 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 9396 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 2688 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting → 222 Kapitel 4 Threads
9100 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 18364 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 11180 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 16.0.6741.2037 shp 0x00920000 C:\Program Files (x86)\Microsoft Office\Root\ Office16\WINWORD.EXE 10.0.10586.122 shp 0x77BF0000 C:\windows\SYSTEM32\ntdll.dll 10.0.10586.0 shp 0x75540000 C:\windows\SYSTEM32\KERNEL32.DLL 10.0.10586.162 shp 0x77850000 C:\windows\SYSTEM32\KERNELBASE.dll 10.0.10586.63 shp 0x75AF0000 C:\windows\SYSTEM32\ADVAPI32.dll ... 10.0.10586.0 shp 0x68540000 C:\Windows\SYSTEM32\VssTrace.DLL 10.0.10586.0 shp 0x5C390000 C:\Windows\SYSTEM32\adsldpc.dll 10.0.10586.122 shp 0x5DE60000 C:\Windows\SYSTEM32\taskschd.dll 10.0.10586.0 shp 0x5E3F0000 C:\Windows\SYSTEM32\srmstormod.dll 10.0.10586.0 shp 0x5DCA0000 C:\Windows\SYSTEM32\srmscan.dll 10.0.10586.0 shp 0x5D2E0000 C:\Windows\SYSTEM32\msdrm.dll 10.0.10586.0 shp 0x711E0000 C:\Windows\SYSTEM32\srm_ps.dll 10.0.10586.0 shp 0x56680000 C:\windows\System32\OpcServices.dll 0x5D240000 C:\Program Files (x86)\Common Files\Microsoft Shared\Office16\WXPNSE.DLL 16.0.6701.1023 shp 0x77E80000 C:\Program Files (x86)\Microsoft Office\Root\ Office16\GROOVEEX.DLL 10.0.10586.0 shp 0x693F0000 C:\windows\system32\dataexchange.dll Der TEB aus Abb. 4–3 ist eine der Datenstrukturen, die sich nicht im System-, sondern im Prozessadressraum befinden. Sein Header ist der Threadinformationsblock (TIB), der hauptsächlich zur Kompatibilität mit OS/2- und Win9x-Anwendungen da ist. Durch die Verwendung eines Anfangs-TIBs ist es auch möglich, Ausnahme- und Stackinformationen beim Erstellen von neuen Threads in kleineren Strukturen unterzubringen. Interne Strukturen von Threads Kapitel 4 223
Stackbasis Stackgrenzwert Threadinformationsblock (TIB) Teilsystem-TIB Client-ID Fiberarten Aktives RPC-Handle Ausnahmeliste TLS-Slots (lokaler Threadspeicher) Zeiger auf sich selbst Letzter Fehlerwert PEB des Elternprozesses PEB GDI-Informationen User32-Clientinformationen OpenGL-Informationen Winsock-Daten Idealer Prozessor ETW-Ablaufverfolgungsdaten COM/OLE-Daten Instrumentierungsdaten WDF-Daten TxF-Daten Zähler für Sperren im eigenen Besitz Abbildung 4–3 Wichtige Felder des Threadumgebungsblocks Im TEB sind Kontextinformationen für den Abbildlader und verschiedene Windows-DLLs gespeichert. Da diese Komponenten im Benutzermodus laufen, brauchen Sie eine Datenstruktur, die vom Benutzermodus aus schreibbar ist. Aus diesem Grund ist diese Struktur im Prozess- und nicht im Systemadressraum untergebracht, wo sie nur im Kernelmodus schreibbar wäre. Die Adresse des TEB können Sie mit dem Kerneldebuggerbefehl !thread herausfinden. Experiment: Den TEB untersuchen Mit dem Befehl !teb des Kernel- oder Benutzermodusdebuggers können Sie die TEB-Struktur ausgeben. Ohne weitere Angaben wird dabei der TEB für den aktuellen Thread des Debuggers angezeigt. Um den TEB eines anderen Threads einzusehen, geben Sie die TEB-Adresse an. Wenn Sie den Kerneldebugger einsetzen, muss der aktuelle Prozess festgelegt sein, bevor Sie den Befehl an die TEB-Adresse ausgeben, damit der richtige Prozesskontext verwendet wird. Wie Sie den TEB mit dem Kerneldebugger ausgeben, erfahren Sie im nächsten Experiment. Zur Anzeige des TEB mit dem Benutzermodusdebugger gehen Sie wie folgt vor: → 224 Kapitel 4 Threads
1. Öffnen Sie WinDbg. 2. Öffnen Sie das Menü File und wählen Sie Run Executable. 3. Wechseln Sie zu C:\Windows\System32\Notepad.exe. Der Debugger sollte am ersten Haltepunkt anhalten. 4. Geben Sie den Befehl !teb ein, um sich den TEB des einzigen zurzeit existierenden Threads anzusehen. Das folgende Beispiel zeigt die Ausgabe auf einem 64-BitWindows-System: 0:000> !teb TEB at 000000ef125c1000 ExceptionList: 0000000000000000 StackBase: 000000ef12290000 StackLimit: 000000ef1227f000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 000000ef125c1000 EnvironmentPointer: 0000000000000000 ClientId: 00000000000021bc . 0000000000001b74 RpcHandle: 0000000000000000 Tls Storage: 00000266e572b600 PEB Address: 000000ef125c0000 LastErrorValue: 0 LastStatusValue: 0 Count Owned Locks: 0 HardErrorMode: 0 5. Geben Sie den Befehl g ein oder drücken Sie (F5), um die Ausführung des Editors fortzusetzen. 6. Klicken Sie im Editor auf Datei und Öffnen und dann auf Abbrechen, um das Öffnen-Dialogfeld zu schließen. 7. Drücken Sie (Strg) + (Untbr) oder öffnen Sie das Menü Debug und wählen Sie Break, um den Prozess zwangsweise anzuhalten. 8. Geben Sie den Befehl ~ (Tilde) ein, um alle Threads in dem Prozess anzuzeigen. Dadurch ergibt sich folgende Ausgabe: 0:005> ~ 0 Id: 21bc.1b74 Suspend: 1 Teb: 000000ef'125c1000 Unfrozen 1 Id: 21bc.640 Suspend: 1 Teb: 000000ef'125e3000 Unfrozen 2 Id: 21bc.1a98 Suspend: 1 Teb: 000000ef'125e5000 Unfrozen 3 Id: 21bc.860 Suspend: 1 Teb: 000000ef'125e7000 Unfrozen 4 Id: 21bc.28e0 Suspend: 1 Teb: 000000ef'125c9000 Unfrozen → Interne Strukturen von Threads Kapitel 4 225
. 5 Id: 21bc.23e0 Suspend: 1 Teb: 000000ef'12400000 Unfrozen 6 Id: 21bc.244c Suspend: 1 Teb: 000000ef'125eb000 Unfrozen 7 Id: 21bc.168c Suspend: 1 Teb: 000000ef'125ed000 Unfrozen 8 Id: 21bc.1c90 Suspend: 1 Teb: 000000ef'125ef000 Unfrozen 9 Id: 21bc.1558 Suspend: 1 Teb: 000000ef'125f1000 Unfrozen 10 Id: 21bc.a64 Suspend: 1 Teb: 000000ef'125f3000 Unfrozen 11 Id: 21bc.20c4 Suspend: 1 Teb: 000000ef'125f5000 Unfrozen 12 Id: 21bc.1524 Suspend: 1 Teb: 000000ef'125f7000 Unfrozen 13 Id: 21bc.1738 Suspend: 1 Teb: 000000ef'125f9000 Unfrozen 14 Id: 21bc.f48 Suspend: 1 Teb: 000000ef'125fb000 Unfrozen 15 Id: 21bc.17bc Suspend: 1 Teb: 000000ef'125fd000 Unfrozen 9. Für jeden Thread wird die TEB-Adresse angezeigt. Um sich einen bestimmten Thread anzusehen, übergeben Sie mit dem Befehl !teb seine TEB-Adresse. Für Thread 9 aus der vorherigen Ausgabe sieht das wie folgt aus: 0:005> !teb 000000ef'125f1000 TEB at 000000ef125f1000 ExceptionList: 0000000000000000 StackBase: 000000ef13400000 StackLimit: 000000ef133ef000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 000000ef125f1000 EnvironmentPointer: 0000000000000000 ClientId: 00000000000021bc . 0000000000001558 RpcHandle: 0000000000000000 Tls Storage: 00000266ea1af280 PEB Address: 000000ef125c0000 LastErrorValue: 0 LastStatusValue: c0000034 Count Owned Locks: 0 HardErrorMode: 0 10. Natürlich ist es auch möglich, sich die tatsächliche Struktur an der TEB-Adresse anzusehen (hier aus Platzgründen gekürzt): 0:005> dt ntdll!_teb 000000ef'125f1000 +0x000 NtTib : _NT_TIB +0x038 EnvironmentPointer : (null) +0x040 ClientId : _CLIENT_ID +0x050 ActiveRpcHandle : (null) +0x058 ThreadLocalStoragePointer : 0x00000266'ea1af280 Void → 226 Kapitel 4 Threads
+0x060 ProcessEnvironmentBlock +0x068 LastErrorValue : 0x000000ef'125c0000 _PEB : 0 +0x06c CountOfOwnedCriticalSections : 0 ... +0x1808 LockCount : 0 +0x180c WowTebOffset : 0n0 +0x1810 ResourceRetValue : 0x00000266'ea2a5e50 Void +0x1818 ReservedForWdf : (null) +0x1820 ReservedForCrt : 0 +0x1828 EffectiveContainerId : _GUID {00000000-0000-0000-0000000000000000} Experiment: Den TEB mit einem Kerneldebugger untersuchen Um sich den TEB mit dem Kerneldebugger anzusehen, gehen Sie wie folgt vor: 1. Suchen Sie den Prozess des Threads, für dessen TEB Sie sich interessieren. Beispielsweise gibt der folgende Befehl die Prozesse von Explorer.exe jeweils mit einer Liste ihrer Threads und einigen grundlegenden Informationen dazu aus (hier gekürzt): lkd> !process 0 2 explorer.exe PROCESS ffffe0012bea7840 SessionId: 2 Cid: 10d8 Peb: 00251000 ParentCid: 10bc DirBase: 76e12000 ObjectTable: ffffc000e1ca0c80 HandleCount: <Data Not Accessible> Image: explorer.exe THREAD ffffe0012bf53080 Cid 10d8.10dc Teb: 0000000000252000 Win32Thread: ffffe0012c1532f0 WAIT: (WrUserRequest) UserMode Non-Alertable ffffe0012c257fe0 SynchronizationEvent THREAD ffffe0012a30f080 Cid 10d8.114c Teb: 0000000000266000 Win32Thread: ffffe0012c2e9a20 WAIT: (UserRequest) UserMode Alertable ffffe0012bab85d0 SynchronizationEvent THREAD ffffe0012c8bd080 Cid 10d8.1178 Teb: 000000000026c000 Win32Thread: ffffe0012a801310 WAIT: (UserRequest) UserMode Alertable ffffe0012bfd9250 NotificationEvent ffffe0012c9512f0 NotificationEvent ffffe0012c876b80 NotificationEvent ffffe0012c010fe0 NotificationEvent ffffe0012d0ba7e0 NotificationEvent ffffe0012cf9d1e0 NotificationEvent ... → Interne Strukturen von Threads Kapitel 4 227
THREAD ffffe0012c8be080 Cid 10d8.1180 Teb: 0000000000270000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable fffff80156946440 NotificationEvent THREAD ffffe0012afd4040 Cid 10d8.1184 Teb: 0000000000272000 Win32Thread: ffffe0012c7c53a0 WAIT: (UserRequest) UserMode Non-Alertable ffffe0012a3dafe0 NotificationEvent ffffe0012c21ee70 Semaphore Limit 0xffff ffffe0012c8db6f0 SynchronizationEvent THREAD ffffe0012c88a080 Cid 10d8.1188 Teb: 0000000000274000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable ffffe0012afd4920 NotificationEvent ffffe0012c87b480 SynchronizationEvent ffffe0012c87b400 SynchronizationEvent ... 2. Gibt es mehr als einen Explorer.exe-Prozess, wählen Sie für die folgenden Schritte einen davon nach Belieben aus. 3. Für jeden Thread wird die TEB-Adresse angezeigt. Da sich der TEB im Benutzerraum befindet, hat diese Adresse nur im Kontext des zugehörigen Prozesses eine Bedeutung. Sie müssen zu dem Prozess/Thread umschalten, den der Debugger sieht. Wählen Sie den ersten Explorerthread aus, da sich dessen Kernelstack wahrscheinlich im physischen Arbeitsspeicher befindet. Bei einem Thread, bei dem das nicht der Fall ist, erhalten Sie eine Fehlermeldung. lkd> .thread /p ffffe0012bf53080 Implicit thread is now ffffe001'2bf53080 Implicit process is now ffffe001'2bea7840 4. Dadurch wird der Kontext auf den angegebenen Thread umgeschaltet (und damit auch auf den entsprechenden Prozess). Jetzt können Sie den Befehl !teb mit der für den Thread angegebenen TEB-Adresse verwenden: lkd> !teb 0000000000252000 TEB at 0000000000252000 ExceptionList: 0000000000000000 StackBase: 00000000000d0000 StackLimit: 00000000000c2000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 0000000000252000 EnvironmentPointer: 0000000000000000 ClientId: 00000000000010d8 . 00000000000010dc RpcHandle: 0000000000000000 Tls Storage: 0000000009f73f30 → 228 Kapitel 4 Threads
PEB Address: 0000000000251000 LastErrorValue: 0 LastStatusValue: c0150008 Count Owned Locks: 0 HardErrorMode: 0 Die CSR_THREAD-Struktur aus Abb. 4–4 entspricht der CSR_PROCESS-Struktur, allerdings für Threads. Wie Sie wissen, wird Letztere von jedem Csrss-Prozess in einer Sitzung unterhalten und gibt die Windows-Teilsystemthreads an, die darin laufen. In der CSR_THREAD-Struktur befinden sich ein Handle, den Csrss für den Thread verwendet, mehrere Flags, die Client-ID (Thread- und Prozess-ID) sowie die Angabe der Threaderstellungszeit. Threads werden bei Csrss registriert, wenn sie ihre erste Nachricht an Csrss senden, gewöhnlich, weil eine API die Benachrichtigung von Scrss über eine Operation oder Bedingung erfordert. Erstellungszeit Threadlink Hashlinks Client-ID Threadhandle Flags Referenzzähler Zähler für Identitätswechsel Abbildung 4–4 Felder der CSR_THREAD-Struktur Experiment: Die CSR_THREAD-Struktur untersuchen Sie können die CSR_THREAD-Struktur mit dem Befehl dt des Kernelmodusdebuggers ausgeben, wenn sich der Debugger im Kontext des Csrss-Prozesses befindet. Befolgen Sie dazu die Anweisungen aus dem Experiment »Die CSR_PROZESS-Struktur untersuchen« aus Kapitel 3. Die folgende Beispielausgabe stammt von einem x64-System mit Windows 10: lkd> dt csrss!_csr_thread +0x000 CreateTime : _LARGE_INTEGER +0x008 Link : _LIST_ENTRY +0x018 HashLinks : _LIST_ENTRY +0x028 ClientId : _CLIENT_ID +0x038 Process : Ptr64 _CSR_PROCESS +0x040 ThreadHandle : Ptr64 Void +0x048 Flags : Uint4B +0x04c ReferenceCount : Int4B +0x050 ImpersonateCount : Uint4B Interne Strukturen von Threads Kapitel 4 229
Die W32THREAD-Struktur aus Abb. 4–5 entspricht der W32PROCESS-Struktur, allerdings für Threads. Sie enthält hauptsächlich Informationen für das GDI-Teilsystem (Pinsel- und Gerätekontextattribute) und DirectX, aber auch für das UMPD-Framework (User-Mode Print Driver), mit dem Hersteller Benutzermodus-Druckertreiber schreiben. Des Weiteren enthält sie den Ren­deringStatus, der für Desktop-Compositing und Antialiasing genutzt wird. Thread Referenzzähler Pinsel/DC-Attribute Benutzermodus-Druckdaten Spritestatus Flags Rendering-/AA-Daten Threadsperre DirectX-Thread Abbildung 4–5 Felder der W32THREAD-Struktur Geburt eines Threads Der Lebenszyklus eines Threads beginnt, wenn ein Prozess (im Kontext eines Threads, z. B. des Threads, der die main-Funktion ausführt) einen neuen Thread erstellt. Die Anforderung geht schließlich an die Windows-Exekutive, wo der Prozess-Manager Speicherplatz für ein Threadobjekt zuweist und den Kernel aufruft, um den Threadsteuerungsblock (KTHREAD) zu initialisieren. Wie bereits erwähnt, führen die verschiedenen Threaderstellungsfunktionen letzten Endes dazu, dass CreateRemoteThreadEx ausgeführt wird. Innerhalb dieser Funktion in Kernel32.dll werden dann die folgenden Schritte ausgeführt, um einen Windows-Thread zu erstellen: 1. Die Funktion wandelt die Windows-API-Parameter in native Flags um und erstellt eine native Struktur, die die Objektparameter beschreibt (OBJECT_ATTRIBUTES, siehe Kapitel 8 in Band 2). 2. Sie erstellt eine Attributliste mit zwei Einträgen, nämlich der Client-ID und der TEB-Adresse (siehe »Der Ablauf von CreateProcess« in Kapitel 3). 3. Sie bestimmt, ob der Thread im aufrufenden Prozess oder in einem anderen Prozess erstellt wird, der durch ein übergebenes Handle bestimmt wird. Ist das Handle gleich dem von GetCurrentProcess zurückgegebenen Pseudohandle (Wert -1), dann ist es derselbe Prozess. Ein anderes Handle kann immer noch ein gültiges Handle für denselben Prozess sein, weshalb NtQueryInformationProcess (in Ntdll) aufgerufen wird, um herauszufinden, ob das der Fall ist. 4. Sie ruft NtCreateThreadEx (in Ntdll) auf, um den Wechsel zur Exekutive im Kernelmodus vorzunehmen. Der Vorgang wird dann in einer Funktion mit demselben Namen und denselben Argumenten fortgesetzt. 230 Kapitel 4 Threads
5. NtCreateThreadEx (in der Exekutive) erstellt und initialisiert den Benutzermodus-Threadkontext (dessen Struktur von der Architektur abhängt) und ruft dann PspCreateThread auf, um ein angehaltenes Exekutivthreadobjekt zu erstellen. (Eine Beschreibung der Schritte, die diese Funktion ausführt, finden Sie in den Phasen 3 und 5 unter »Der Ablauf von Cre­ ateProcess« in Kapitel 3.) Danach gibt die Funktion die Steuerung zurück und landet wieder im Benutzermodus bei CreateRemoteThreadEx. 6. CreateRemoteThreadEx weist einen Aktivierungskontext für den Thread zu, der von der Side-­ by-Side-Assemblyunterstützung verwendet wird. Anschließend fragt sie den Aktivierungsstack ab, um zu sehen, ob eine Aktivierung erforderlich ist, und führt diese bei Bedarf durch. Der Aktivierungsstackzeiger wird im TEB des neuen Threads gespeichert. 7. Sofern der Aufrufer den Thread nicht mit dem Flag CREATE_SUSPENDED erstellt hat, wird der Thread jetzt aufgenommen, sodass er zur Ausführung eingeplant werden kann. Wenn ein Thread zu laufen beginnt, werden die im Abschnitt »Phase 7: Prozessinitialisierung im Kontext des neuen Prozesses durchführen« von Kapitel 3 beschriebenen Schritte ausgeführt, bevor die angegebene Startadresse des eigentlichen Benutzers aufgerufen wird. 8. Das Threadhandle und die Thread-ID werden an den Aufrufer zurückgegeben. Die Threadaktivität untersuchen Eine Untersuchung der Threadaktivität ist vor allem dann wichtig, wenn Sie herauszufinden versuchen, warum ein Hostprozess für mehrere Dienste (wie Svchost.exe, Dllhosts.exe oder Lsass.exe) läuft oder warum ein Prozess nicht mehr reagiert. Es gibt eine Reihe von Werkzeugen, die einen Blick auf verschiedene Elemente des Status von Windows-Threads zulassen, z. B. WinDbg (beim Anschluss an einen Benutzerprozess und im Kerneldebuggingmodus), die Leistungsüberwachung und Process Explorer. (Werkzeuge zur Anzeige von Threadplanungsinformationen sind im Abschnitt »Threadplanung« angegeben.) Um mit Process Explorer die Threads in einem Prozess einzusehen, doppelklicken Sie auf den Prozess, um sein Eigenschaftendialogfeld zu öffnen. Alternativ können Sie auch auf den Prozess rechtsklicken und Properties wählen. Wechseln Sie anschließend zur Registerkarte Threads. Sie zeigt eine Liste der Threads in dem Prozess sowie vier Spalten mit Informationen darüber an: die ID des Threads, seinen prozentualen Anteil der CPU-Nutzung (auf der Grundlage des eingestellten Aktualisierungsintervalls), die Anzahl der ihm zugeschlagenen Zyklen und seine Startadresse. Sie können die Anzeige nach allen vier Spalten sortieren. Neu erstellte Threads werden grün hervorgehoben, Threads, die beendet werden, in Rot. (Die Dauer dieser Hervorhebung können Sie im Menü Options mit Difference Highlight Dura­ tion ändern.) Das kann praktisch sein, um eine unnötige Threaderstellung in einem Prozess aufzuspüren. (Im Allgemeinen sollten Threads beim Start des Prozesses erstellt werden und nicht jedes Mal, wenn in einem Prozess eine Anforderung verarbeitet wird.) Wenn Sie einen Thread in der Liste auswählen, zeigt Process Explorer jeweils die Thread-ID, den Startzeitpunkt, den Status, die CPU-Zeit, die Anzahl der Zyklen, die Anzahl der Kontextwechsel, den idealen Prozessor und seine Gruppe sowie die E/A-, die Speicher-, die Basis- und die aktuelle (dynamische) Priorität an. Mit der Schaltfläche Kill können Sie einen einzelnen Die Threadaktivität untersuchen Kapitel 4 231
Thread beenden, allerdings sollten Sie dabei äußerste Vorsicht walten lassen. Die Schaltfläche Suspend unterbindet die fortgesetzte Ausführung eines Threads und verhindert so, dass ein außer Kontrolle geratener Thread CPU-Zeit in Anspruch nimmt. Dies kann jedoch zu Deadlocks führen, weshalb auch diese Schaltfläche ebenso mit Vorsicht zu genießen ist wie Kill. Mit der Schaltfläche Permissions schließlich können Sie den Sicherheitsdeskriptor des Threads einsehen (siehe Kapitel 7). Anders als der Task-Manager und andere Prozess- und Prozessorüberwachungswerkzeuge verwendet der Process Explorer einen Taktzykluszähler für die Threadlaufzeitbestimmung (weiter hinten in diesem Kapitel erklärt) statt eines Taktintervalltimers. Daher wird im Process Explorer eine erheblich abweichende Sicht der CPU-Nutzung angezeigt. Viele Threads laufen so kurz, dass nur selten (wenn überhaupt) der gerade ausgeführte Thread gesehen wird, wenn ein Interrupt für einen Taktintervalltimer auftritt. Daher werden sie für einen Großteil ihrer CPUZeit gar nicht berücksichtigt, weshalb Werkzeuge mit solchen Timern eine CPU-Zeit von 0 % messen. Die Gesamtzahl der Taktzyklen dagegen entspricht der Anzahl der Prozessorzyklen, die für die Threads in dem Prozess aufgelaufen sind. Sie hängt von der Auflösung des Taktintervalltimers an, da der Zähler bei jedem Zyklus intern vom Prozessor geführt und bei jedem Interrupt von Windows aktualisiert wird. (Vor einem Kontextwechsel erfolgt eine abschließende Akkumulation.) Die Threadstartadresse wird im Format Modul! Funktion angezeigt, wobei es sich bei Modul um den Namen der .exe- oder .dll-Datei handelt. Für den Funktionsnamen ist ein Zugriff auf die Symboldateien für das Modul erforderlich (siehe das Experiment »Einzelheiten über Prozesse mit Process Explorer einsehen« in Kapitel 1). Wenn Ihnen nicht klar ist, worum es sich bei dem Modul handelt, klicken Sie auf die Schaltfläche Module, um das Eigenschaftendialogfeld für die Explorerdatei des Moduls mit der Startadresse des Threads zu öffnen (z. B. die .exe- oder .dll-Datei). Bei Threads, die mit der Windows-Funktion CreateThread erstellt wurden, zeigt Process Explorer die an CreateThread übergebene Funktion und nicht die tatsächliche Startfunktion des Threads an. Das liegt daran, dass Windows-Threads mit einer gemeinsamen Threadstart-Wrapperfunktion beginnen (TrlUserThreadStart in Ntdll.dll). Würde Process Explorer die tatsächliche Startadresse anzeigen, dann sähe es so aus, als begännen die meisten Threads in den Prozessen an derselben Adresse, und das wäre nicht sehr hilfreich, um herauszufinden, welchen Code der Thread ausführt. Falls Process Explorer die vom Benutzer angegebene Startadresse nicht abrufen kann (etwa bei einem geschützten Prozess), zeigt er jedoch die Wrapperfunktion RtlUserThreadStart an. Die angezeigte Threadstartadresse reicht unter Umständen nicht aus, um genau festzustellen, was der Thread tut und welche Komponenten in dem Prozess für seinen CPU-Verbrauch verantwortlich sind. Das ist vor allem dann der Fall, wenn die Threadstartadresse eine generische Startfunktion ist, etwa wenn der Funktionsname nicht angibt, was der Thread tatsächlich macht. In diesem Fall kann eine Untersuchung des Threadstacks die Antwort liefern. Dazu doppelklicken Sie auf den betreffenden Thread (oder wählen ihn aus und klicken auf die Schaltfläche 232 Kapitel 4 Threads
Stack). Process Explorer zeigt dann den Stack des Threads an (bei einem Thread im Kernelmodus sowohl den Benutzer- als auch den Kernelstack). Einen Benutzermodusdebugger (WinDbg, Ntsd und Cdb) können Sie an einen Prozess anhängen, um sich den Benutzerstack eines Threads anzeigen zu lassen. In Process Explorer dagegen müssen Sie lediglich auf eine Schaltfläche klicken, um sowohl den Benutzer- als auch den Kernelstack zu sehen. Wie die beiden nächsten Experimente zeigen, können Sie den Benutzer- und den Kernelthreadstack auch mit WinDbg im lokalen Kerneldebuggingmodus untersuchen. Bei der Untersuchung von 32-Bit-Prozessen, die als Wow64-Prozesse auf 64-Bit-Systemen laufen (siehe Kapitel 8 in Band 2), zeigt Process Explorer sowohl den 32- als auch den 64-Bit-Stack für die Threads an. Da ein Thread zum Zeitpunkt des echten (64-Bit-)Systemaufrufs bereits zum 64-Bit-Stack und -Kontext gewechselt ist, würde die Betrachtung des 64-Bit-Stacks nur die halbe Geschichte zeigen, nämlich den 64-Bit-Part des Threads mit dem Thunkcode von Wow64. Bei der Untersuchung von Wow64-Prozessen müssen Sie daher auf jeden Fall sowohl den 32als auch den 64-Bit-Stack berücksichtigen. Experiment: Einen Threadstack mit einem Benutzermodusdebugger anzeigen Gehen Sie wie folgt vor, um WinDbg an einen Prozess anzuhängen und Informationen über den Thread und dessen Stack einzusehen: 1. Führen Sie Notepad.exe und WinDbg.exe aus. 2. Öffnen Sie in WinDbg das Menü File und wählen Sie Attach to Process. 3. Suchen Sie die Notepad.exe-Instanz und klicken Sie auf OK, um den Debugger anzuhängen. Der Debugger sollte jetzt die Ausführung des Editors anhalten. 4. Führen Sie die Threads in dem Prozess mit dem Befehl ~ auf. Für jeden Thread werden die Debugger-ID, die Client-ID (Prozess-ID.Thread-ID), der Unterbrechungszähler (dieser Wert sollte meistens 1 lauten, da der Thread aufgrund des Haltepunktes unterbrochen wurde), die TEB-Adresse und die Angabe angezeigt, ob der Thread mit einem Debuggerbefehl eingefroren wurde. 0:005> ~ 0 Id: 612c.5f68 Suspend: 1 Teb: 00000022'41da2000 Unfrozen 1 Id: 612c.5564 Suspend: 1 Teb: 00000022'41da4000 Unfrozen 2 Id: 612c.4f88 Suspend: 1 Teb: 00000022'41da6000 Unfrozen 3 Id: 612c.5608 Suspend: 1 Teb: 00000022'41da8000 Unfrozen 4 Id: 612c.cf4 Suspend: 1 Teb: 00000022'41daa000 Unfrozen . 5 Id: 612c.9f8 Suspend: 1 Teb: 00000022'41db0000 Unfrozen → Die Threadaktivität untersuchen Kapitel 4 233
5. Beachten Sie den Punkt, der in der Ausgabe vor Thread 5 steht. Er kennzeichnet den aktuellen Debuggerthread. Um den Aufrufstack einzusehen, geben Sie den Befehl k ein: 0:005> k # Child-SP RetAddr Call Site 00 00000022'421ff7e8 00007ff8'504d9031 ntdll!DbgBreakPoint 01 00000022'421ff7f0 00007ff8'501b8102 ntdll!DbgUiRemoteBreakin+0x51 02 00000022'421ff820 00007ff8'5046c5b4 KERNEL32!BaseThreadInitThunk+0x22 03 00000022'421ff850 00000000'00000000 ntdll!RtlUserThreadStart+0x34 6. Der Debugger hat in den Notepad-Prozess einen Thread injiziert, der eine Haltepunktanweisung ausgibt (DbgBreakPoint). Um sich den Aufrufstack eines anderen Threads anzusehen, verwenden Sie den Befehl ~nk, wobei n die in WinDbg angezeigte Threadnummer ist. (Der aktuelle Debuggerthread wird dadurch nicht geändert.) Das folgende Beispiel zeigt die Ausgabe für Thread 2: 0:005> ~2k # Child-SP RetAddr Call Site 00 00000022'41f7f9e8 00007ff8'5043b5e8 ntdll!ZwWaitForWorkViaWorkerFactory+0x14 01 00000022'41f7f9f0 00007ff8'501b8102 ntdll!TppWorkerThread+0x298 02 00000022'41f7fe00 00007ff8'5046c5b4 KERNEL32!BaseThreadInitThunk+0x22 03 00000022'41f7fe30 00000000'00000000 ntdll!RtlUserThreadStart+0x34 7. Um den Debugger auf einen anderen Thread umzuschalten, verwenden Sie den Befehl ~ns (wobei n wiederum die Threadnummer ist). Im folgenden Beispiel schalten wir zu Thread 0 um und zeigen dessen Stack an: 0:005> ~0s USER32!ZwUserGetMessage+0x14: 00007ff8'502e21d4 c3 ret 0:000> k # Child-SP RetAddr Call Site 00 00000022'41e7f048 00007ff8'502d3075 USER32!ZwUserGetMessage+0x14 01 00000022'41e7f050 00007ff6'88273bb3 USER32!GetMessageW+0x25 02 00000022'41e7f080 00007ff6'882890b5 notepad!WinMain+0x27b 03 00000022'41e7f180 00007ff8'341229b8 notepad!__mainCRTStartup+0x1ad 04 00000022'41e7f9f0 00007ff8'5046c5b4 KERNEL32!BaseThreadInitThunk+0x22 05 00000022'41e7fa20 00000000'00000000 ntdll!RtlUserThreadStart+0x34 8. 234 Auch wenn sich der Thread zurzeit im Kernelmodus befindet, zeigt ein Benutzermodusdebugger die letzte Funktion im Benutzermodus an (ZwUserGetMessage in der vorstehenden Ausgabe). Kapitel 4 Threads
Experiment: Einen Threadstack mit einem lokalen Kernelmodusdebugger anzeigen In diesem Experiment verwenden wir einen lokalen Kerneldebugger, um den Benutzer- und den Kernelmodusstack eines Threads einzusehen. Als Beispiel nutzen wir dafür einen der Threads des Explorers, aber Sie können dieses Experiment auch mit anderen Prozessen und Threads ausprobieren. 1. Zeigen Sie alle Prozesse an, die im Abbild Explorer.exe ausgeführt werden. (Wenn Sie die Explorer-Option Ordnerfenster in eigenem Prozess starten gewählt haben, wird mehr als eine Instanz des Explorers angezeigt. Ein Prozess verwaltet den Desktop und die Taskleiste, der andere die Explorer-Fenster.) lkd> !process 0 0 explorer.exe PROCESS ffffe00197398080 SessionId: 1 Cid: 18a0 Peb: 00320000 ParentCid: 1840 DirBase: 17c028000 ObjectTable: ffffc000bd4aa880 HandleCount: <Data Not Accessible> Image: explorer.exe PROCESS ffffe00196039080 SessionId: 1 Cid: 1f30 Peb: 00290000 ParentCid: 0238 DirBase: 24cc7b000 ObjectTable: ffffc000bbbef740 HandleCount: <Data Not Accessible> Image: explorer.exe 2. Wählen Sie eine Instanz aus und lassen Sie sich die Threadübersicht dafür anzeigen: lkd> !process ffffe00196039080 2 PROCESS ffffe00196039080 SessionId: 1 Cid: 1f30 Peb: 00290000 ParentCid: 0238 DirBase: 24cc7b000 ObjectTable: ffffc000bbbef740 HandleCount: <Data Not Accessible> Image: explorer.exe THREAD ffffe0019758f080 Cid 1f30.0718 Teb: 0000000000291000 Win32Thread: ffffe001972e3220 WAIT: (UserRequest) UserMode Non-Alertable ffffe00192c08150 SynchronizationEvent THREAD ffffe00198911080 Cid 1f30.1aac Teb: 00000000002a1000 Win32Thread: ffffe001926147e0 WAIT: (UserRequest) UserMode Non-Alertable ffffe00197d6e150 SynchronizationEvent ffffe001987bf9e0 SynchronizationEvent → Die Threadaktivität untersuchen Kapitel 4 235
THREAD ffffe00199553080 Cid 1f30.1ad4 Teb: 00000000002b1000 Win32Thread: ffffe0019263c740 WAIT: (UserRequest) UserMode Non-Alertable ffffe0019ac6b150 NotificationEvent ffffe0019a7da5e0 SynchronizationEvent THREAD ffffe0019b6b2800 Cid 1f30.1758 Teb: 00000000002bd000 Win32Thread: 0000000000000000 WAIT: (Suspended) KernelMode Non-Alertable SuspendCount 1 ffffe0019b6b2ae0 NotificationEvent ... 3. Wechseln Sie in den Kontext des ersten Threads in dem Prozess (Sie können auch andere Threads auswählen): lkd> .thread /p /r ffffe0019758f080 Implicit thread is now ffffe001'9758f080 Implicit process is now ffffe001'96039080 Loading User Symbols .............................................. 4. Schauen Sie sich jetzt den Thread an, um seine Einzelheiten und seinen Aufrufstack zu untersuchen (Adressen hier gekürzt): lkd> !thread ffffe0019758f080 THREAD ffffe0019758f080 Cid 1f30.0718 Teb: 0000000000291000 Win32Thread : ffffe001972e3220 WAIT : (UserRequest)UserMode Non - Alertable ffffe00192c08150 SynchronizationEvent Not impersonating DeviceMap ffffc000b77f1f30 Owning Process ffffe00196039080 Image : explorer.exe Attached Process N / A Image : N / A Wait Start TickCount 17415276 Ticks : 146 (0:00 : 00 : 02.281) Context Switch Count 2788 UserTime 00 : 00 : 00.031 KernelTime 00 : 00 : 00.000 IdealProcessor : 4 *** WARNING : Unable to verify checksum for C : \windows\explorer.exe Win32 Start Address explorer!wWinMainCRTStartup(0x00007ff7b80de4a0) Stack Init ffffd0002727cc90 Current ffffd0002727bf80 Base ffffd0002727d000 Limit ffffd00027277000 Call 0000000000000000 Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5 → 236 Kapitel 4 Threads
... Call Site ... nt!KiSwapContext + 0x76 ... nt!KiSwapThread + 0x15a ... nt!KiCommitThreadWait + 0x149 ... nt!KeWaitForSingleObject + 0x375 ... nt!ObWaitForMultipleObjects + 0x2bd ... nt!NtWaitForMultipleObjects + 0xf6 ... nt!KiSystemServiceCopyEnd + 0x13 (TrapFrame @ ffffd000'2727cb00) ... ntdll!ZwWaitForMultipleObjects + 0x14 ... KERNELBASE!WaitForMultipleObjectsEx + 0xef ... USER32!RealMsgWaitForMultipleObjectsEx + 0xdb ... USER32!MsgWaitForMultipleObjectsEx + 0x152 ... explorerframe!SHProcessMessagesUntilEventsEx + 0x8a ... explorerframe!SHProcessMessagesUntilEventEx + 0x22 ... explorerframe!CExplorerHostCreator::RunHost + 0x6d ... explorer!wWinMain + 0xa04fd ... explorer!__wmainCRTStartup + 0x1d6 Einschränkungen für Threads in geschützten Prozessen Wie bereits in Kapitel 3 erwähnt, bestehen bei geschützten Prozessen (was sowohl klassische geschützte Prozesse als auch PPLs einschließt) starke Einschränkungen, was die Zugriffsrechte angeht, selbst für Benutzer mit den höchsten Rechten auf dem System. Diese Einschränkungen gelten auch für die Threads in diesen Prozessen. Dadurch wird sichergestellt, dass der Code, der in einem geschützten Prozess läuft, durch Windows-Standardfunktionen nicht gekapert oder auf andere Weise beeinträchtigt werden kann, denn dazu müssten diese Funktionen Zugriffsrechte bekommen, die für Threads in geschützten Prozessen nicht vergeben werden. Die einzigen gewährten Berechtigungen sind THREAD_SUSPEND_RESUME und THREAD_SET/QUERY_LIMITED_INFORMATION. Experiment: Informationen über Threads in geschützten Prozessen mit Process Explorer anzeigen In diesem Experiment schauen Sie sich Informationen über die Threads in einem geschützten Prozess an. Gehen Sie dazu wie folgt vor: 1. Suchen Sie in der Prozessliste einen geschützten oder einen PPL-Prozess, z. B. Audiodg. exe oder Csrss.exe. → Die Threadaktivität untersuchen Kapitel 4 237
2. Öffnen Sie das Eigenschaftendialogfeld des Prozesses und klicken Sie auf die Registerkarte Threads. 3. Process Explorer zeigt nicht die Win32-Threadstartadresse an, sondern stattdessen den standardmäßigen Threadstartwrapper in Ntdll.dll. Wenn Sie auf die Schaltfläche Stack klicken, wird eine Fehlermeldung angezeigt, da Process Explorer dazu den virtuellen Speicher innerhalb des geschützten Prozesses lesen müsste, was er aber nicht tun kann. 4. Es werden zwar die Basis- und die dynamische Priorität angezeigt, die E/A- und die Speicherpriorität aber nicht (genauso wenig wie die Zyklen). Auch das ist ein Beispiel für das eingeschränkte Zugriffsrecht THREAD_QUERY_LIMITED_INFORMATION im Gegensatz zu dem vollständigen Informationszugriffsrecht THREAD_QUERY_INFORMATION. 5. Versuchen Sie, einen Thread in dem geschützten Prozess zu beenden. Auch dabei erhalten Sie eine Fehlermeldung, da Sie nicht über das Zugriffsrecht THREAD_TERMINATE verfügen. Threadplanung Dieser Abschnitt erläutert die Planungsrichtlinien und -algorithmen von Windows. Die ersten Teilabschnitte geben eine knappe Beschreibung der Threadplanung in Windows und eine Definition der wichtigsten Begriffe. Anschließend werden die Windows-Prioritätsstufen sowohl für Windows-APIs als auch für den Windows-Kernel erklärt. Nach einer Übersicht über die 238 Kapitel 4 Threads
Windows-Hilfsprogramme und -Werkzeuge rund um die Threadplanung werden die Datenstrukturen und Algorithmen des Windows-Planungssystems ausführlich vorgestellt. Dabei werden auch gängige Planungssituationen beschrieben und erklärt, wie die Thread- und die Prozessorauswahl erfolgen. Überblick über die Threadplanung in Windows Windows verwendet ein prioritätsgesteuertes, präemptives Planungssystem. Es wird immer mindestens einer der ausführbaren (betriebsbereiten) Threads höchster Priorität ausgeführt, wobei einige davon jedoch nur auf bestimmten Prozessoren laufen dürfen oder bevorzugt auf bestimmten Prozessoren ausgeführt werden sollten, was als Prozessoraffinität bezeichnet wird. Eine solche Affinität besteht immer zu einer ganzen Gruppe von Prozessoren, die bis zu 64 Prozessoren umfassen kann. Normalerweise können Threads nur auf verfügbaren Prozessoren in der mit dem Prozess verbundenen Gruppe ausgeführt werden. (Dies dient zur Kompatibilität mit älteren Windows-Versionen, die nur 64 Prozessoren unterstützen.) Entwickler können die Prozessoraffinität durch entsprechende APIs sowie über die Affinitätsmaske im Abbildheader ändern. Benutzer können Werkzeuge einsetzen, um die Affinität zur Laufzeit oder beim Erstellen des Prozesses zu ändern. Threads in einem Prozess können zwar unterschiedlichen Gruppen zugeordnet werden, doch ein Thread kann nur auf den Prozessoren seiner Gruppe ausgeführt werden. Entwickler haben auch die Möglichkeit, Anwendungen zu schreiben, die mithilfe einer erweiterten Planungs-API die Affinitäten ihrer Threads mit logischen Prozessoren in verschiedenen Gruppen verknüpfen. Dadurch wird aus dem Prozess ein Mehrgruppenprozess, dessen Threads theoretisch auf jedem verfügbaren Prozessor auf dem Computer ausgeführt werden können. Nachdem ein Thread ausgewählt wurde, wird er eine bestimmte Zeit lang ausgeführt, bis ein anderer Thread derselben Priorität an der Reihe ist. Dieser Zeitraum wird als Quantum bezeichnet, wobei sich die Quantumswerte zwischen den einzelnen Systemen und Prozessen aus drei Gründen unterscheiden: QQ Systemkonfigurationseinstellungen (langes oder kurzes Quantum, festes oder variables, Trennung von Prioritäten) QQ Vordergrund- oder Hintergrundstatus des Prozesses QQ Änderung des Quantums durch das Jobobjekt Diese Einzelheiten werden im Abschnitt über Quanten weiter hinten in diesem Kapitel ausführlich erklärt. Es kann jedoch sein, dass ein Thread gar nicht dazu kommt, sein Quantum komplett auszufüllen, da Windows einen präemptiven Scheduler verwendet. Wenn ein Thread mit höherer Priorität zur Ausführung bereitsteht, kann der aktuelle Thread vorzeitig unterbrochen werden, bevor er eine Zeitscheibe komplett genutzt hat. Es ist sogar möglich, dass ein Thread zur Ausführung ausgewählt, aber schon vor Beginn seines Quantums abgebrochen wird. Der Windows-Planungscode ist im Kernel implementiert. Es gibt jedoch kein eigenes Modul und keine eigene Routine dafür. Der Code ist über die Stellen im Kernel verstreut, in denen Ereignisse rund um die Threadplanung auftreten können. Die Routinen für diese Aufgaben Threadplanung Kapitel 4 239
werden gesammelt als Dispatcher des Kernels bezeichnet. Die folgenden Ereignisse können eine solche Threaddisponierung erfordern: QQ Ein Thread ist zur Ausführung bereit. Das kann etwa der Fall sein, wenn ein Thread neu erstellt wurde oder einen Wartezustand verlassen hat. QQ Ein Thread verlässt den Ausführungsstatus, da sein Zeitquantum abgelaufen ist, er beendet wird, er die Ausführung aufgebt oder er in einen Wartezustand eintritt. QQ Die Priorität eines Threads ändert sich, entweder aufgrund eines Systemdienstaufrufs oder weil Windows selbst den Prioritätswert geändert hat. QQ Die Prozessoraffinität eines Threads ändert sich, sodass er nicht mehr auf dem Prozessor ausgeführt werden kann, auf dem er läuft. Bei jedem dieser Ereignisse muss Windows entscheiden, welcher Thread als Nächstes auf dem logischen Prozessor ausgeführt werden soll, auf dem der betreffende Thread lief, bzw. auf welchem logischen Prozessor der Thread jetzt laufen soll. Nachdem ein logischer Prozessor für den neuen Thread ausgewählt wurde, erfolgt dort schließlich ein Kontextwechsel zu diesem Thread. Dabei wird der flüchtige Prozessorstatus des laufenden Threads gespeichert, der flüchtige Status eines anderen Threads geladen und die Ausführung des neuen Threads gestartet. Windows nimmt die Planung für einzelne Threads vor. Das ist sinnvoll, da Prozesse nicht ausgeführt werden, sondern nur die Ressourcen und den Kontext für die Ausführung ihrer Threads liefern. Da Planungsentscheidungen rein auf Threadebene erfolgen, wird dabei nicht berücksichtigt, zu welchen Prozessen die Threads gehören. Wenn beispielsweise Prozess A über zehn ausführbare Threads verfügt und Prozess B über zwei, wobei alle zwölf Threads dieselbe Priorität haben, dann kann jeder Thread theoretisch ein Zwölftel der CPU-Zeit erhalten. Windows vergibt also nicht 50 % der CPU-Zeit an Prozess A und die anderen 50 % an Prozess B. Prioritätsstufen Für ein Verständnis der Threadplanungsalgorithmen sind zunächst einmal Kenntnisse der Prioritätsstufen in Windows erforderlich. Wie Abb. 4–6 zeigt, verwendet Windows intern 32 Prioritätsstufen von 0 bis 31 (wobei 31 die höchste Priorität dargestellt). Die Werte sind in folgende Bereiche aufgeteilt: QQ 16 Echtzeitstufen (16 bis 31) QQ 16 variable Stufen (0 bis 15), wobei Stufe 0 für den Nullseitenthread reserviert ist (siehe Kapitel 5) 240 Kapitel 4 Threads
Echtzeit Dynamisch (normal) Nullseitenthread Abbildung 4–6 Threadprioritätsstufen Threadprioritätsstufen werden von der Windows-API und vom Windows-Kernel zugewiesen. Die Windows-API ordnet die Prozesse zunächst nach der Prioritätsklasse, der sie bei ihrer Erstellung zugeordnet wurden (wobei die Zahl in Klammern der interne PROCESS_PRIORITY_CLASS-Index ist, den der Kernel erkennt): QQ Echtzeit (4) QQ Hoch (3) QQ Höher als normal (6) QQ Normal (2) QQ Niedriger als normal (5) QQ Leerlauf (1) Mit der Windows-API SetPriorityClass kann die Prioritätsklasse eines Prozesses auf einen dieser Werte eingestellt werden. Anschließend wird den einzelnen Threads in den Prozessen eine relative Priorität zugewiesen. Hier stehen die Zahlen für den Abstand zur Basispriorität des Prozesses: QQ Zeitkritisch (15) QQ Am höchsten (2) QQ Höher als normal (1) QQ Normal (0) QQ Niedriger als normal (-1) QQ Am niedrigsten (-2) QQ Leerlauf (-15) Die Prioritätsstufen Zeitkritisch und Leerlauf (+15 und -15) werden als Sättigungswerte bezeichnet und stellen besondere Stufen dar und keine echten Offsets. Diese Werte können an die Windows-API SetThreadPriority übergeben werden, um die relative Priorität eines Threads zu ändern. In der Windows-API hat daher jeder Thread eine Basispriorität, die von der Prozessprioritätsklasse und seiner relativen Threadpriorität abhängt. Im Kernel werden die bereits erwähnten PROCESS_PRIORITY_CLASS-Indizes mithilfe des globalen Arrays PspPriorityTable in die Basisprio- Threadplanung Kapitel 4 241
ritäten 4, 8, 13, 14, 6 oder 10 umgesetzt. (Hierbei handelt es sich um eine feste Zuordnung, die nicht geändert werden kann.) Anschließend wird die relative Threadpriorität als Differenz dieser Basispriorität zugeschlagen. Beispielsweise erhält ein Thread mit der höchsten Priorität dadurch eine Threadbasispriorität, die zwei Stufen höher liegt als die Basispriorität seines Prozesses. Die Zuordnung der Windows-Prioritäten zu den internen numerischen Windows-Prioritäten wird in Abb. 4–7 und Tabelle 4–1 dargestellt. Normale (dynamische) Prioritäten Echtzeitprioritäten Prioritätsklasse »Echtzeit« Prioritätsklasse »Hoch« Prioritätsklasse »Höher als normal« Prioritätsklasse »Normal« Prioritätsklasse »Niedriger als normal« Prioritätsklasse »Leerlauf« Priorität Abbildung 4–7 In der Windows-API verwendete Threadprioritäten Prioritätsklasse (relative Priorität) Echtzeit Hoch Höher als normal Normal Niedriger als normal Leerlauf Zeitkritisch (+Sättigung) 31 15 15 15 15 15 Am höchsten (+2) 26 15 12 10 8 6 Höher als normal (+1) 25 14 11 9 7 5 Normal (0) 24 13 10 8 6 4 Niedriger als normal (-1) 23 12 9 7 5 3 Am niedrigsten (-2) 22 11 8 6 4 2 Leerlauf (-Sättigung) 16 1 1 1 1 1 Tabelle 4–1 Zuordnung der Windows-Kernelprioritäten zu den Windows-API-Prioritäten Wie Sie sehen, behalten die relativen Threadprioritäten Echtzeit und Leerlauf ihre Werte unabhängig von der Prozessprioritätsklasse bei (sofern diese nicht Echtzeit lautet). Das liegt daran, dass die Windows-API die Sättigung der Priorität vom Kernel anfordert, indem sie +16 oder -16 als gewünschte relative Priorität übergibt. Die Formel zur Berechnung dieser Werte lautet wie folgt (wobei HIGH_PRIORITY gleich 31 ist): If Time-Critical: ((HIGH_PRIORITY+1) / 2 If Idle: -((HIGH_PRIORITY+1) / 2 242 Kapitel 4 Threads
Diese Werte werden vom Kernel als Anforderung zur Sättigung erkannt, sodass das Feld ­Saturation in der KTHREAD-Struktur ausgefüllt wird. Bei positiver Sättigung erhält der Thread dadurch die höchstmögliche Priorität in seiner Prioritätsklasse (dynamisch oder Echtzeit), bei negativer Sättigung die niedrigste. Spätere Anforderungen zur Änderung der Basispriorität des Prozesses haben keine Auswirkungen mehr auf die Basispriorität dieser Threads, da gesättigte Threads im Verarbeitungscode übersprungen werden. Wie Tabelle 4–1 zeigt, gibt es für Threads sieben mögliche Windows-API-Prioritätsstufen (sechs bei der Prioritätsklasse Hoch). In der Prioritätsklasse Echtzeit können Sie sogar alle Prioritätsstufen zwischen 16 und 31 einstellen (siehe Abb. 4–7). Die Werte, die nicht von den in der Tabelle gezeigten Konstanten abgedeckt werden, können mit den Argumenten -7, -6, -5, -4, -3, 3, 4, 5 und 6 für SetThreadPriority festgelegt werden. (Weitere Informationen erhalten Sie im Abschnitt »Echtzeitprioritäten« weiter hinten.) Für den Scheduler spielt es jedoch keine Rolle, wie die Priorität (als Kombination der Prozessprioritätsklasse und der relativen Threadpriorität) durch die Windows-API gebildet wurde. Für ihn zählt nur das Endergebnis. Beispielsweise gibt es zwei Möglichkeiten, die Prioritätsstufe 10 zu erreichen, nämlich erstens durch einen Prozess der Prioritätsklasse Normal (8) und einen Thread mit der relativen Priorität Am höchsten (+2) und zweitens durch einen Prozess der Prioritätsklasse Höher als normal (10) und einen Thread mit der relativen Priorität Normal (0). Der Scheduler sieht nur den Wert 10, weshalb die Threads für ihn die gleiche Priorität aufweisen. Ein Prozess hat nur einen Basisprioritätswert, ein Thread dagegen eine aktuelle (dynamische) und eine Basispriorität. Planungsentscheidungen werden aufgrund der aktuellen Priorität getroffen. Wie in dem Abschnitt »Prioritätserhöhung« weiter hinten erklärt, kann das System unter bestimmten Umständen die Priorität eines Threads kurzzeitig im dynamischen Bereich (1 bis 15) heraufsetzen. Dagegen ändert Windows niemals die Threadpriorität im Echtzeitbereich (16 bis 31), weshalb bei solchen Threads die Basis- und die aktuelle Priorität stets identisch sind. Ein Thread erbt seine ursprüngliche Basispriorität von seinem Prozess, der seine Basispriorität wiederum standardmäßig von seinem Erstellerprozess erbt. Dieses Verhalten können Sie in der Funktion CreateProcess und mit dem Befehl start ändern. Es ist auch möglich, die Priorität eines Prozesses nachträglich zu ändern. Dazu können Sie die Funktion SetPriorityClass sowie verschiedene Werkzeuge verwenden, in denen sie zur Verfügung steht, darunter der Task-Manager und Process Explorer. (Dazu rechtsklicken Sie auf den Prozess und wählen die neue Prioritätsklasse aus.) Beispielsweise können Sie die Priorität eines CPU-intensiven Prozesses verringern, damit er die normalen Systemaktivitäten nicht beeinträchtigt. Bei der Änderung der Prozesspriorität wird die Priorität seiner Threads entsprechend herauf- oder heruntergesetzt, wobei deren relative Prioritäten jedoch bestehen bleiben. Anwendungen und Dienste starten gewöhnlich mit der Basispriorität Normal, weshalb ihr ursprünglicher Thread normalerweise auf der Prioritätsstufe 8 ausgeführt wird. Einige Windows-Systemprozesse (wie der Sitzungs-Manager, der Dienststeuerungs-Manager und der lokale Sicherheitsauthentifizierungsprozess haben jedoch eine leicht höhere Basisprozesspriorität als Normal, weshalb auch die darin enthaltenen Threads mit einer höheren Priorität als dem Standardwert 8 starten. Threadplanung Kapitel 4 243
Echtzeitprioritäten In jeder Anwendung können Sie die Threadprioritäten im dynamischen Bereich erhöhen oder senken. Um in den Echtzeitbereich vorzudringen, müssen Sie allerdings das Recht für die Planungspriorität erhöhen (SetIncreaseBasePriorityPrivilege). Beachten Sie aber, dass im Echtzeitprioritätsbereich viele wichtige Kernelmodus-Systemthreads von Windows laufen. Wenn Ihre Threads erheblich viel Zeit in diesem Bereich verbringen, können sie dadurch wichtige Systemfunktionen blockieren (etwa den Speicher-Manager, den Cache-Manager oder einige Gerätetreiber). Wenn ein Prozess mit den Standard-Windows-APIs in den Echtzeitbereich verlegt wurde, müssen alle seine Threads (selbst Leerlaufthreads) auf einer der Echtzeitprioritätsstufen laufen. Mit den Standardschnittstellen ist es daher nicht möglich, in einem Prozess Echtzeit- und dynamische Threads zu kombinieren. Das liegt daran, dass die API SetThreadPriority die native API NtSetInformationThread mit der Informationsklasse ThreadBasePriority aufruft, bei der Prioritäten im selben Bereich bleiben müssen. Außerdem dürfen Prioritätsänderungen bei der Verwendung dieser Informationsklasse nur die von Windows-APIs verstandenen Abstände von -2 bis 2 (oder zeitkritisch/Leerlauf) umfassen, es sei denn, die Anforderung kommt von Csrss oder einem anderen Echtzeitprozess. Anders ausgedrückt, ein Echtzeitprozess kann beliebige Threadprioritäten zwischen 16 und 31 auswählen, auch wenn die relativen Threadprioritäten der Windows-Standard-API die Auswahl auf die Werte aus der zuvor gezeigten Tabelle einzuschränken scheinen. Wie bereits erwähnt, führt ein Aufruf von SetThreadPriority mit einigen besonderen Werten dazu, dass NtSetInformationThread mit der Informationsklasse ThreadActualBasePriority aufgerufen wird. Die Kernelbasispriorität für den Thread kann dann direkt festgelegt werden, wobei es auch möglich ist, die Priorität eines Threads in einem Echtzeitprozess im dynamischen Bereich einzustellen. Die Bezeichnung Echtzeit bedeutet nicht, dass es sich bei Windows um ein Echtzeitbetriebssystem im üblichen Sinne des Wortes handelt. Schließlich bietet es keine Echtzeitfunktionen wie garantierte Interruptlatenz oder eine garantierte Ausführungszeit für Threads an. Echtzeit bedeutet hier nur »mit einer höheren Priorität als alle anderen«. Werkzeuge für den Umgang mit Prioritäten Die Basisprozesspriorität können Sie im Task-Manager und Process Explorer einsehen und ändern. In Process Manager können Sie auch einzelne Threads in einem Prozess beenden (wobei Sie natürlich äußerste Vorsicht walten lassen müssen). Die Prioritäten einzelner Prozesse können Sie in der Leistungsüberwachung, in Process Explorer und WinDbg einsehen. Es kann durchaus sinnvoll sein, die Priorität eines Prozesses zu erhöhen oder zu verringern, aber die Einstellung der Priorität einzelner Threads in einem Prozess ist nicht sinnvoll, weil nur jemand, der das Programm in allen Einzelheiten kennt (also der Entwickler), die relative Wichtigkeit der Threads innerhalb des Prozesses überblicken kann. 244 Kapitel 4 Threads
Die einzige Möglichkeit, um die Anfangsprioritätsklasse für einen Prozess anzugeben, bietet der Befehl start in der Windows-Eingabeaufforderung. Wenn Sie ein Programm immer mit einer bestimmten Priorität starten lassen wollen, können Sie einen Kurzbefehl definieren, um immer den Befehl start zu verwenden. Dieser Kurzbefehl muss mit cmd /c beginnen, um die Eingabeaufforderung zu starten. Anschließend führt er den angegebenen Befehl aus und beendet die Eingabeaufforderung. Um beispielsweise den Editor mit der Leerlauf-Prozesspriorität auszuführen, verwenden Sie den Befehl cmd /c start /low Notepad.exe. Experiment: Prozess- und Threadprioritäten untersuchen und festlegen Um Prozess- und Threadprioritäten zu untersuchen und festzulegen, gehen Sie wie folgt vor: 1. Führen Sie Notepad.exe ganz normal aus, etwa indem Sie an der Eingabeaufforderung notepad eingeben. 2. Öffnen Sie den Task-Manager und klicken Sie auf die Registerkarte Details. 3. Fügen Sie die Spalte Basispriorität hinzu. Das ist die Bezeichnung der Prioritätsklasse im Task-Manager. 4. Suchen Sie in der Liste nach Notepad: 5. Wie Sie sehen, läuft der Notepad-Prozess mit der Prioritätsklasse Normal (8). Die Prioritätsklasse Leerlauf wird im Task-Manager als Niedrig angezeigt. 6. Öffnen Sie Process Explorer. 7. Doppelklicken Sie auf den Prozess Notepad, um sein Eigenschaftendialogfeld aufzurufen, und klicken Sie auf die Registerkarte Threads. → Threadplanung Kapitel 4 245
8. Markieren Sie den ersten Thread (falls es mehrere gibt). Sie sollten jetzt folgende Anzeige sehen: 9. Achten Sie auf die Prioritäten des Threads: Seine Basispriorität beträgt nach wie vor 8, seine aktuelle (dynamische) Priorität dagegen 10. Den Grund dafür erfahren Sie im Abschnitt »Prioritätserhöhung« weiter hinten. 10. Wenn Sie wollen, können Sie den Thread unterbrechen und beenden (wobei Sie jedoch in beiden Fällen sehr vorsichtig sein müssen). 11. Rechtsklicken Sie im Task-Manager auf den Prozess Notepad, wählen Sie Priorität festlegen und dann Hoch: → 246 Kapitel 4 Threads
12. Bestätigen Sie den Vorgang in dem daraufhin eingeblendeten Dialogfeld. In Process Explorer können Sie jetzt erkennen, dass die Priorität des Threads in die Klasse Hoch (13) heraufgesetzt wurde. Die dynamische Priorität hat die gleiche relative Steigerung erfahren: 13. Ändern Sie die Prioritätsklasse im Task-Manager auf Echtzeit. Dazu müssen Sie ein Administrator auf dem Computer sein. Diese Änderung können Sie auch in Process Manager vornehmen. 14. In Process Manager betragen die Basis- und die dynamische Priorität des Threads jetzt beide 24. Wie Sie wissen, wendet der Kernel auf Threads im Echtzeitbereich niemals Prioritätserhöhungen an. Threadplanung Kapitel 4 247
Der Windows-Systemressourcen-Manager Windows Server 2012 R2 Standard Edition und höhere SKUs enthalten den optional in­ stallierbaren Windows-Systemressourcen-Manager (WSRM). Damit können Administratoren Richtlinien aufstellen, um Affinitätseinstellungen und Grenzwerte für die CPU- und Speichernutzung (sowohl physisch als auch virtuell) durch Prozesse festzulegen. Darüber hinaus kann WSRM Ressourcennutzungsberichte erstellen, die sich zur Buchführung und als Nachweis für Dienstleistungsverträge mit Benutzern nutzen lassen. Die Richtlinien können auf bestimmte Anwendungen angewandt werden (durch Vergleich des Abbildnamens mit oder ohne Befehlszeilenargumente), aber auch auf Benutzer oder Gruppen. Sie lassen sich auch zeitlich planen, sodass sie nur zu bestimmten Zeiten in Kraft treten oder ständig gelten. Nachdem Sie eine Ressourcenzuweisungsrichtlinie aufgestellt haben, überwacht der WSRM-Dienst die CPU-Nutzung der darüber verwalteten Prozesse und passt deren Basisprioritäten an, wenn sie ihre Sollwerte nicht erfüllen. Bei den Grenzwerten für den physischen Arbeitsspeicher wird mit der Funktion SetProcessWorkingSetSizeEx ein harter Höchstwert für die Größe des Arbeitssatzes festgelegt. Beim Grenzwert für den virtuellen Arbeitsspeicher prüft der Dienst, wie viel privaten virtuellen Arbeitsspeicher die Prozesse verbrauchen (siehe Kapitel 5). Wird der Grenzwert überschritten, kann WSRM je nach Einstellung entweder den betreffenden Prozess beenden oder einen Eintrag in das Ereignisprotokoll schreiben. Damit lässt sich ein Prozess mit einem Speicherleck aufspüren, bevor er den gesamten verfügbaren, zugesicherten Speicher auf dem System verbraucht. Die WSRM-Speichergrenzwerte gelten jedoch nicht für AWE-Speicher (Address Windowing Extensions), Arbeitsspeicher mit großen Seiten und den Kernelspeicher (nicht ausgelagerter und ausgelagerter Pool). Mehr darüber erfahren Sie in Kapitel 5. Threadstatus Bevor wir uns mit den Threadplanungsalgorithmen beschäftigen, müssen wir zunächst die verschiedenen Ausführungsstatus ansehen, in denen sich ein Thread befinden kann: QQ Bereit (Ready) Ein Thread in diesem Status wartet darauf, dass er ausgeführt oder nach Abschluss eines Wartevorgangs wieder eingewechselt wird. Bei der Suche nach auszuführenden Threads berücksichtigt der Dispatcher nur solche, die bereit sind. QQ Verzögert bereit (Deferred Ready) Dieser Status wird für Threads verwendet, die auf einem bestimmten Prozessor ausgewählt wurden, aber noch nicht begonnen haben, dort zu laufen. Dieser Status ist dazu da, dass der Kernel die Zeit verkürzen kann, die eine prozessorweise Sperre für die Planungsdatenbank aufrechterhalten wird. QQ Standby Ein Thread in diesem Zustand wurde dazu ausgewählt, als nächster auf einem bestimmten Prozessor zu laufen. Wenn die erforderlichen Bedingungen hergestellt sind, führt der Dispatcher einen Kontextwechsel zu diesem Thread durch. Auf jedem Prozessor des Systems kann sich immer nur ein Thread auf einmal im Standby-Zustand befinden. 248 Kapitel 4 Threads
Es ist übrigens möglich, dass ein Thread noch im Standby-Status unterbrochen wird, also bevor er mit der Ausführung begonnen hat (etwa wenn ein Thread mit höherer Priorität inzwischen lauffähig wurde). QQ Wird ausgeführt (Running) Nachdem der Dispatcher einen Kontextwechsel zu einem Thread durchgeführt hat, geht dieser Thread in den Zustand Wird ausgeführt über. Der Thread wird so lange ausgeführt, bis sein Quantum abläuft (und ein anderer Thread derselben Priorität bereit zur Ausführung ist), er durch einen Thread höherer Priorität unterbrochen wird, er beendet wird, er die Ausführung abgibt oder er freiwillig in einen Wartezustand eintritt. QQ Wartend (Waiting) Ein Thread kann auf verschiedene Weisen in den Wartezustand übergehen: Er kann freiwillig auf ein Objekt warten, um die Ausführung zu synchronisieren; das Betriebssystem kann für den Thread warten (etwa um eine Auslagerungs-E/A durchzuführen); und ein Umgebungsteilsystem kann den Thread anweisen, sich selbst anzuhalten. Wenn die Wartezeit des Threads endet, wird er je nach seiner Priorität entweder sofort weiter ausgeführt oder in den Status Bereit versetzt. QQ Übergang (Transition) Ein Thread tritt in den Übergangsstatus ein, wenn er zur Ausführung bereit ist, sein Kernelstack aber aus dem Arbeitsspeicher ausgelagert wurde. Nachdem der Kernelstack wieder in den Arbeitsspeicher zurückgebracht wurde, nimmt der Thread den Status Bereit an. (Threadstacks werden in Kapitel 5 erklärt.) QQ Abgebrochen (Terminated) Wenn ein Thread die Ausführung beendet, tritt er in diesen Status ein. Ob nach seiner Beendigung die Zuweisung des Exekutivthreadobjekts (der Datenstruktur im Systemarbeitsspeicher, die den Thread beschreibt) aufgehoben wird oder nicht, bestimmt der Objekt-Manager. Beispielsweise bleibt das Objekt bestehen, wenn es noch offene Handles zu dem Thread gibt. Ein Thread kann auch in den Status Abgebrochen übergehen, wenn er ausdrücklich von einem anderen Thread beendet wird, etwa durch den Aufruf der Windows-API TerminateThread. QQ Initialisiert (Initialized) verwendet. Dieser Status wird intern während der Erstellung des Threads Abb. 4–8 zeigt die wichtigsten Statusübergänge für Threads. Die Zahlen stehen für die internen Werte der einzelnen Zustände, die in Werkzeugen wie der Leistungsüberwachung angezeigt werden. Bereit und Verzögert bereit werden als ein Status dargestellt, da Verzögert bereit lediglich als vorübergehender Platzhalter für Planungsroutinen dient. Das gilt auch für den Status Standby. Diese Zustände sind fast immer sehr kurzlebig. Threads in diesem Status gehen immer schnell in Bereit, Wird ausgeführt oder Wartend über. Threadplanung Kapitel 4 249
Initialisierung (0) Vorzeitige Unterbrechung, Ablauf des Quantums Vorzeitige Unterbrechung Bereit (1), verzögert bereit (7) Einwechseln des Kernelstacks Wird ausgeführt (2) Standby (3) Freiwilliger Wechsel Übergang (6) Wartend (5) Abgebrochen (4) Auslagerung des Kernelstacks Abbildung 4–8 Threadstatus und Übergänge Experiment: Statuswechsel bei der Threadplanung Die Statuswechsel bei der Threadplanung können Sie mit der Windows-Leistungsüberwachung beobachten. Dieses Werkzeug ist sehr praktisch, wenn Sie eine Multithreadanwendung debuggen wollen und sich über den Status der Threads in den Prozessen nicht im Klaren sind. Zur Beobachtung der Statuswechsel gehen Sie wie folgt vor: 1. Laden Sie das CPU Stress Tool aus den Begleitmaterialien zu diesem Buch herunter. 2. Starten Sie CPUStres.exe. Thread 1 sollte jetzt aktiv sein. 3. Aktivieren Sie Thread 2, indem Sie ihn in der Liste markieren und auf Activate klicken oder indem Sie auf ihn rechtsklicken und im Kontextmenü Activate auswählen. Sie sollten jetzt eine ähnliche Anzeige wie die folgende sehen: → 250 Kapitel 4 Threads
4. Klicken Sie auf die Startschaltfläche und geben Sie perfmon ein, um die Leistungsüberwachung zu starten. 5. Wählen Sie ggf. die Diagrammansicht aus und entfernen Sie den vorhandenen CPU-Indikator. 6. Rechtsklicken Sie auf das Diagramm und wählen Sie Eigenschaften. 7. Klicken Sie auf die Registerkarte Grafik und ändern Sie das Maximum für die vertikale Skalierung auf 7 (da die einzelnen Status mit den Zahlen 0 bis 7 bezeichnet werden). Klicken Sie auf OK. 8. Klicken Sie in der Symbolleiste auf Hinzufügen, um das Dialogfeld Leistungsindikatoren hinzufügen zu öffnen. 9. Wählen Sie das Leistungsobjekt Thread und darunter den Indikator Threadstatus aus. 10. Aktivieren Sie das Kontrollkästchen Beschreibung anzeigen, um eine Definition der Werte einzublenden: 11. Wählen Sie <Alle Instanzen> im Feld Instanzen. Geben Sie dann cpustres ein und klicken Sie auf Suchen. 12. Wählen Sie die ersten drei Threads von cpustres aus (cpustres/0, cpustres/1 und cpu­ stres/2), klicken Sie auf Hinzufügen >> und dann auf OK. Thread 0 befindet sich im Status 5 (Wartend), da dies der GUI-Thread ist, der auf Benutzereingaben wartet. Die Threads 1 und 2 wechseln ständig zwischen den Status 2 und 5 (Wird ausgeführt und Wartend). Es kann sein, dass Thread 2 hinter Thread 1 verborgen ist, da beide mit derselben Priorität und derselben Aktivität laufen. → Threadplanung Kapitel 4 251
13. Rechtsklicken Sie in CPU Stress auf Thread 2 und wählen Sie im Kontextmenü unter Activity den Punkt Busy. Jetzt wird Thread 2 häufiger in Status 2 (Wird ausgeführt) angezeigt als Thread 1: → 252 Kapitel 4 Threads
14. Rechtsklicken Sie auf Thread 1 und wählen Sie den Aktivitätsgrad Maximum. Wiederholen Sie diesen Schritt für Thread 2. Jetzt sind beide Threads ständig in Status 2, da sie praktisch in einer Endlosschleife ausgeführt werden: Auf einem Einzelprozessorsystem sehen Sie etwas anderes. Da es nur einen Prozessor gibt, kann immer nur ein Thread auf einmal ausgeführt werden, sodass die beiden Threads zwischen Status 1 (Bereit) und 2 (Wird ausgeführt) wechseln: → Threadplanung Kapitel 4 253
15. Wenn Sie auf einem Mehrprozessorsystem arbeiten (was wahrscheinlicher ist), können Sie den gleichen Effekt dadurch erzielen, dass Sie im Task-Manager auf den Prozess CPUSTRES rechtsklicken, Zugehörigkeit festlegen wählen und dann nur einen einzigen Prozessor auswählen, egal welchen. (Das können Sie auch in CPU Stress selbst tun, indem Sie das Menü Process öffnen und Affinity auswählen.) 16. Es gibt noch eine weitere Sache, die Sie ausprobieren können. Nachdem Sie die Einstellung aus Punkt 15 vorgenommen haben, rechtsklicken Sie in CPU Stress auf Thread 1 und wählen die Priorität Above normal. Jetzt wird Thread 1 kontinuierlich ausgeführt (Status 2), während sich Thread 2 immer im Bereitschaftsstatus (1) befindet. Das liegt daran, dass es nur einen Prozessor gibt, weshalb im Allgemeinen der Thread mit höherer Priorität den Zuschlag erhält. Von Zeit zu Zeit werden Sie jedoch sehen, dass Thread 1 in den Bereitschaftsstatus übergeht, weil der abgehängte Thread ca. alle vier Sekunden eine Prioritätserhöhung erhält, sodass er kurzzeitig laufen kann. Allerdings wird diese Statusänderung in dem Diagramm oft nicht angezeigt, da die Leistungsüberwachung nur eine Auflösung von einer Sekunde hat, was zu grob ist. Eine ausführlichere Beschreibung dieses Vorgangs finden Sie weiter hinten in diesem Kapitel im Abschnitt »Prioritätserhöhung«. 254 Kapitel 4 Threads
Die Dispatcherdatenbank Um Threadplanungsentscheidungen treffen zu können, unterhält der Kernel eine Reihe von Datenstrukturen, die zusammengenommen als Dispatcherdatenbank bezeichnet werden. Darin bewahrt der Kernel den Überblick darüber, welche Threads auf Ausführung waren und auf welchen Prozessoren welche Threads laufen. Um die Skalierbarkeit zu verbessern und eine parallele Threaddisponierung zu ermöglichen, gibt es in einem Windows-Mehrprozessorsystem Bereitschaftswarteschlangen für die einzelnen Prozessoren und gemeinsam genutzte Bereitschaftswarteschlangen für Prozessorgruppen (siehe Abb. 4–9). Dadurch kann jede CPU in der für sie zuständigen Bereitschaftswarteschlange nach dem nächsten auszuführenden Thread suchen, ohne die systemweiten Bereitschaftswarteschlangen sperren zu müssen. Gruppe 1 (CPU 0, 1, 2) Bereitschaftswarteschlangen Gruppe 2 (CPU 3, 4, 5) Bereitschaftswarteschlangen Bereitschaftsübersicht Bereitschaftsübersicht Abbildung 4–9 Dispatcherdatenbank in einem Windows-Mehrprozessorsystem. In diesem Beispiel werden sechs Prozessoren verwendet. P steht für Prozesse, T für Threads. In Windows-Versionen vor Windows 8 und Windows Server 2012 hatte jeder Prozessor seine eigene Bereitschaftswarteschlange und seine eigene Bereitschaftsübersicht. Sie waren im Prozessorsteuerungsblock (PRCB) gespeichert. (Die Felder des PRCB können Sie mit dem Befehl dt nt!_kprcb im Kerneldebugger einsehen.) Beginnend mit Windows 8 und Windows Server 2012 werden gemeinsam genutzte Bereitschaftswarteschlangen und Bereitschaftsübersichten jeweils für eine ganze Prozessorgruppe verwendet. Dadurch kann das System besser entscheiden, welcher Prozessor der Gruppe als nächster verwendet werden soll. Die prozessoreigenen Bereitschaftswarteschlangen sind jedoch nach wie vor vorhanden und werden für Threads mit Affinitätseinschränkungen verwendet. Threadplanung Kapitel 4 255
Die gemeinsamen Datenstrukturen müssen (durch ein Spinlock) geschützt werden. Damit die Konkurrenz um die Warteschlangen auf einem unerheblichen Niveau bleibt, dürfen die Gruppen daher nicht zu groß werden. In der aktuellen Implementierung beträgt die maximale Gruppengröße vier Prozessoren. Gibt es mehr als vier logische Prozessoren, so werden weitere Gruppen gebildet und die verfügbaren Prozessoren gleichmäßig darauf verteilt. Beispielsweise werden auf einem Sechs-Prozessor-System zwei Gruppen zu je drei Prozessoren angelegt. Die Bereitschaftswarteschlangen, die Bereitschaftsübersichten und einige andere Informationen werden in einer Kernelstruktur namens KSHARED_READY_QUEUE gespeichert, die sich wiederum im PRCB befindet. Es gibt sie zwar für jeden Prozessor, doch es wird immer nur die Struktur auf dem jeweils ersten Prozessor einer Gruppe von allen Prozessoren der Gruppe gemeinsam verwendet. Die Bereitschaftswarteschlangen (ReadListHead in KSHARED_READY_QUEUE) enthalten die Threads, die sich im Bereitschaftszustand befinden und darauf warten, dass sie zur Ausführung eingeplant werden. Es gibt eine Warteschlange für jede der 32 Prioritätsstufen. Um die Auswahl der auszuführenden oder zu unterbrechenden Threads zu beschleunigen, unterhält Windows eine 32-Bit-Maske, die als Bereitschaftsübersicht (ReadySummary) bezeichnet wird. Jedes gesetzte Bit darin steht für einen oder mehr Threads in der Bereitschaftswarteschlange für die jeweilige Prioritätsstufe (Bit 0 steht für Priorität 0, Bit 1 für Priorität 1 usw.). Anstatt die einzelnen Bereitschaftslisten darauf zu untersuchen, ob sie leer sind oder nicht (was die Planungsentscheidungen von der Anzahl an Threads mit unterschiedlicher Priorität abhängig machen würde), wird mit einem nativen Prozessorbefehl ein einziger Bitscan durchgeführt, um das höchste gesetzte Bit zu finden. Dieser Vorgang dauert unabhängig von der Anzahl der Threads in der Bereitschaftswarteschlange immer gleich lang. Die Dispatcherwarteschlange wird durch Erhöhung der IRQL auf DISPATCH_LEVEL (2) synchronisiert. (Eine Beschreibung der Interrupt-Prioritätsstufen oder IRQLs finden Sie in Kapitel 6.) Dadurch wird verhindert, dass andere Threads die Threaddisponierung auf dem Prozessor unterbrechen, da Threads normalerweise auf IRQL 0 oder 1 laufen. Es ist jedoch noch mehr erforderlich, als nur die IRQL hochzusetzen, denn es kann sein, dass andere Prozessoren gleichzeitig eine Erhöhung auf dieselbe IRQL vornehmen, um ihre eigene Dispatcherdatenbank zu bearbeiten. Wie Windows den Zugriff auf die Dispatcherdatenbank synchronisiert, wird weiter hinten in diesem Kapitel im Abschnitt »Mehrprozessorsysteme« erklärt. Experiment: Ausführungsbereite Threads anzeigen Mit dem Kerneldebuggerbefehl !ready können Sie sich eine Liste der Threads anzeigen lassen, die auf den einzelnen Prioritätsstufen zur Ausführung bereit sind. Das folgende Beispiel wurde auf einem 32-Bit-Computer mit vier logischen Prozessoren erstellt: 0: kd> !ready KSHARED_READY_QUEUE 8147e800: (00) ****---------------------------- → 256 Kapitel 4 Threads
SharedReadyQueue 8147e800: Ready Threads at priority 8 THREAD 80af8bc0 Cid 1300.15c4 Teb: 7ffdb000 Win32Thread: 00000000 READY on processor 80000002 THREAD 80b58bc0 Cid 0454.0fc0 Teb: 7f82e000 Win32Thread: 00000000 READY on processor 80000003 SharedReadyQueue 8147e800: Ready Threads at priority 7 THREAD a24b4700 Cid 0004.11dc Teb: 00000000 Win32Thread: 00000000 READY on processor 80000001 THREAD a1bad040 Cid 0004.096c Teb: 00000000 Win32Thread: 00000000 READY on processor 80000001 SharedReadyQueue 8147e800: Ready Threads at priority 6 THREAD a1bad4c0 Cid 0004.0950 Teb: 00000000 Win32Thread: 00000000 READY on processor 80000002 THREAD 80b5e040 Cid 0574.12a4 Teb: 7fc33000 Win32Thread: 00000000 READY on processor 80000000 SharedReadyQueue 8147e800: Ready Threads at priority 4 THREAD 80b09bc0 Cid 0004.12dc Teb: 00000000 Win32Thread: 00000000 READY on processor 80000003 SharedReadyQueue 8147e800: Ready Threads at priority 0 THREAD 82889bc0 Cid 0004.0008 Teb: 00000000 Win32Thread: 00000000 READY on processor 80000000 Processor 0: No threads in READY state Processor 1: No threads in READY state Processor 2: No threads in READY state Processor 3: No threads in READY state Zu den Prozessornummern wurde 0x8000000 addiert, doch die eigentlichen Prozessornummern lassen sich leicht erkennen. Die erste Zeile zeigt die Adresse der Struktur KSHARED_READY_ QUEUE mit der Gruppennummer in Klammern an (00 in der vorstehenden Ausgabe). Darauf folgt eine grafische Darstellung der Prozessoren in dieser Gruppe (die vier Sternchen). Kurioserweise geben die letzten vier Zeilen an, dass es keine ausführungsbereiten Threads gibt, was der vorstehenden Ausgabe widerspricht. Diese Zeilen beziehen sich jedoch auf die ausführungsbereiten Threads in dem alten PRCB-Element DispatcherReadyListHead, da für Threads mit eingeschränkter Affinität (die nur auf einigen der Prozessoren in der Gruppe laufen dürfen) die Bereitschaftswarteschlangen der einzelnen Prozessoren herangezogen werden. Sie können auch KSHARED_READY_QUEUE anzeigen lassen, indem Sie die Adresse angeben, die Sie bei der Ausführung des Befehls !ready gewonnen haben: 0: kd> dt nt!_KSHARED_READY_QUEUE 8147e800 +0x000 Lock : 0 +0x004 ReadySummary : 0x1d1 +0x008 ReadyListHead : [32] _LIST_ENTRY [ 0x82889c5c - 0x82889c5c ] → Threadplanung Kapitel 4 257
+0x108 RunningSummary : [32] "???" +0x128 Span : 4 +0x12c LowProcIndex : 0 +0x130 QueueIndex : 1 +0x134 ProcCount : 4 +0x138 Affinity : 0xf Das Element ProcCount gibt die Anzahl der Prozessoren in der Gruppe an (in diesem Beispiel sind es vier). Beachten Sie auch den Wert von ReadySummary. Er lautet hier 0x1d1, was im Binärformat 111010001 beträgt. Wenn Sie die Bits von rechts nach links lesen, können Sie erkennen, dass es ausführungsbereite Threads auf den Prioritätsstufen 0, 4, 6, 7 und 8 gibt, was der vorherigen Ausgabe entspricht. Das Quantum Wie bereits erwähnt, ist das Quantum die Dauer, für die ein Thread laufen darf, bevor Windows prüft, ob ein anderer Thread derselben Priorität auf Ausführung wartet. Wenn ein Thread sein Quantum ausgeschöpft hat und es keine anderen Threads seiner Priorität gibt, erlaubt Windows ihm, für ein weiteres Quantum zu laufen. Auf Clientversionen von Windows werden Threads standardmäßig zwei Taktintervalle lang ausgeführt, auf Serversystemen zwölf Taktintervalle lang. (Wie Sie diese Werte ändern, erfahren Sie im Abschnitt »Das Quantum steuern«.) Der Grund für den längeren Standardwert auf Serversystemen ist die Reduzierung von Kontextwechseln. Aufgrund des längeren Quantums haben Serveranwendungen, die durch Clientanforderungen aufgeweckt werden, eine größere Chance, die Anforderung abzuschließen und wieder in den Wartezustand zu wechseln, bevor ihr Quantum endet. Die Länge der Taktintervalle hängt von der Hardwareplattform ab. Die Frequenz der Taktinterrupts wird von der HAL bestimmt, nicht vom Kernel. So beträgt das Taktintervall auf den meisten x86-Einzelprozessorsystemen etwa 10 ms (wobei Windows auf solchen Computern nicht mehr läuft; wir erwähnen sie hier nur als Beispiel) und auf den meisten x86- und x64-Mehrprozessorsystemen etwa 15 ms. Das Taktintervall wird in der Kernelvariablen KeMaximumIncrement in der Einheit von hundert Nanosekunden gespeichert. Threads laufen zwar in Einheiten von Taktintervallen, doch das System zieht die Anzahl der Ticks nicht als Maß dafür heran, wie lange ein Thread schon ausgeführt wird und ob sein Quantum erschöpft ist. Das liegt daran, dass die Laufzeitverwaltung eines Threads auf Prozessorzyklen basiert. Wenn das System startet, multipliziert es die Prozessorgeschwindigkeit (CPU-Taktzyklen pro Sekunde) in Hertz mit der Anzahl der Sekunden für einen Tick (auf der Grundlage des zuvor beschriebenen Wertes von KeMaximumIncrement), um die Anzahl der Taktzyklen eines Quantums zu berechnen. Dieser Wert wird in der Kernelvariablen KiCyclesPerClockQuantum gespeichert. Daher werden Threads nicht eine bestimmte Anzahl von Ticks lang ausgeführt. Ihre Laufzeit richtet sich nach einem Quantumssollwert. Dabei handelt es sich um einen Schätzwert dafür, wie viele CPU-Taktzyklen der Thread verbraucht haben wird, wenn seine Zeit abgelaufen 258 Kapitel 4 Threads
ist. Dieser Zielwert sollte gleich einer entsprechenden Anzahl von Taktintervall-Timerticks sein, da die Berechnung der Taktzyklen pro Quantum auf der Taktintervall-Timerfrequenz beruht. Das können Sie im folgenden Experiment überprüfen. Beachten Sie jedoch, dass Interruptzyklen nicht dem Thread zugeschlagen werden, sodass die tatsächliche Taktzeit länger sein kann. Experiment: Die Taktintervallfrequenz bestimmen Die Windows-Funktion GetSystemTimeAdjustment gibt das Taktintervall zurück. Um es zu bestimmen, führen Sie das Sysinternals-Tool clockres aus. Die folgende Ausgabe wurde auf einem 64-Bit-Quadcore-System mit Windows 10 erstellt: C:\>clockres ClockRes v2.0 - View the system clock resolution Copyright (C) 2009 Mark Russinovich SysInternals - www.sysinternals.com Maximum timer interval: 15.600 ms Minimum timer interval: 0.500 ms Current timer interval: 1.000 ms Aufgrund von Multimedia-Timern kann das aktuelle Intervall kleiner ausfallen als das maximale (Standard-)Intervall. Solche Timer werden bei Funktionen wie timeBeginPeriod und timeSetEvent verwendet, die dazu dienen, Callbacks in Intervallen von bestenfalls 1 ms zu empfangen. Das verursacht eine globale Umprogrammierung des Kernelintervalltimers, sodass der Scheduler häufiger aufwacht, was die Systemleistung herabsetzen kann. Wie Sie im nächsten Abschnitt sehen werden, hat das jedoch keinerlei Auswirkungen auf die Quantumslängen. Es ist auch möglich, den Wert wie folgt in der globalen Kernelvariablen KeMaximum­ Increment zu lesen (wobei dieses Beispiel auf einem anderen System erstellt wurde als das vorherige): 0: kd> dd nt!KeMaximumIncrement L1 814973b4 0002625a 0: kd> ? 0002625a Evaluate expression: 156250 = 0002625a Dies entspricht dem Standardwert von 15,6 ms. Die Bestimmung der Quantumslänge Im Prozesssteuerungsblock (KPROCESS) jedes Prozesses ist ein Quantumsrücksetzwert angegeben. Er wird verwendet, wenn in dem Prozess neue Threads erstellt werden, und wird in den Threadsteuerungsblock (KTHREAD) kopiert, der dann dazu herangezogen wird, um dem Thread einen neuen Quantumssollwert zu geben. Gespeichert wird der Rücksetzwert in Form Threadplanung Kapitel 4 259
von Quantumseinheiten (mehr darüber in Kürze), die dann mit der Anzahl der Taktzyklen pro Quantum multipliziert werden, um den Quantumssollwert zu berechnen. Während der Ausführung eines Threads werden bei den verschiedenen Ereignissen wie Kontextwechseln, Interrupts und bestimmten Planungsentscheidungen CPU-Taktzyklen angerechnet. Wenn bei einem Interrupt des Taktintervalltimers die Anzahl dieser angerechneten CPU-Taktzyklen den Quantumssollwert erreicht oder überschreitet, wird die Quantumsendverarbeitung ausgelöst. Wartet ein anderer Thread mit der gleichen Priorität auf Ausführung, so erfolgt ein Kontextwechsel zum nächsten Thread in der Bereitschaftswarteschlange. Intern wird eine Quantumseinheit als ein Drittel eines Ticks dargestellt. Ein Tick entspricht also drei Quanten. Das bedeutet, dass Threads auf Windows-Clientsystemen einen Quantumsrücksetzwert von 6 haben (2 * 3) und auf Serversystemen von 36 (12 * 3). Aus diesem Grund wird der Wert n KiCyclesPerClockQuantum am Ende der zuvor beschriebenen Berechnung durch 3 geteilt, da der ursprüngliche Wert nur die CPU-Taktzyklen pro Taktintervall-Timertick angibt. Quanten werden intern als Bruchteil eines Ticks statt eines gesamten Ticks gespeichert, damit auf Versionen vor Windows Vista ein »Verfall beim Warten« in Teilquanten abgeschlossen werden kann. In früheren Zeiten wurde der Taktintervalltimer benutzt, um den Ablauf von Quanten zu bestimmen. Ohne diese Anpassung wäre es unter Umständen möglich gewesen, dass das Quantum eines Threads niemals abnimmt. Stellen Sie sich beispielsweise vor, ein Thread wird ausgeführt, geht in einen Wartezustand über, wird erneut ausgeführt und geht abermals in einen Wartezustand über, wobei er aber niemals gerade dann läuft, wenn der Taktintervalltimer ausgelöst wird. In diesem Fall würde sein Quantum niemals um die Zeit verringert, in der er tatsächlich ausgeführt wurde. Da den Threads jetzt CPU-Taktzyklen statt Quanten angerechnet werden und der Vorgang nicht mehr vom Taktintervalltimer abhängt, ist diese Anpassung jedoch nicht mehr nötig. Experiment: Die Taktzyklen pro Quantum bestimmen Windows macht die Anzahl der Taktzyklen pro Quantum in keiner Funktion zugänglich. Mithilfe der angegebenen Berechnungen und Beschreibungen können Sie diesen Wert jedoch auf die folgende Weise selbst herausfinden. Dazu benötigen Sie einen Kerneldebugger wie WinDbg im lokalen Debuggingmodus. 1. Bestimmen Sie die von Windows erkannte Prozessorfrequenz. Dazu können Sie mit dem Befehl !cpuinfo den Wert des Feldes MHz im PRCB ausgeben. Das folgende Beispiel stammt von einem Vier-Prozessor-System mit 2794 MHz: lkd> !cpuinfo CP F/M/S Manufacturer MHz PRCB Signature MSR 8B Signature Features 0 6,60,3 GenuineIntel 2794 ffffffff00000000 >ffffffff00000000< a3cd3fff 1 6,60,3 GenuineIntel 2794 ffffffff00000000 a3cd3fff 2 6,60,3 GenuineIntel 2794 ffffffff00000000 a3cd3fff 3 6,60,3 GenuineIntel 2794 ffffffff00000000 a3cd3fff → 260 Kapitel 4 Threads
2. Rechnen Sie den Wert in Hertz um. Dadurch erhalten Sie die Anzahl der CPU-Takt­ zyklen pro Sekunde, in diesem Fall 2.794.000.000. 3. Rufen Sie mithilfe von clockres das Taktintervall auf Ihrem System ab. Es gibt an, wie lange es dauert, bis der Timer ausgelöst wird. Auf unserem Beispielsystem betrug der Wert 15,625 ms. 4. Rechnen Sie diesen Wert in die Anzahl von Auslösungen des Taktintervalltimers pro Sekunde um. Da eine Sekunde 1000 ms umfasst, müssen Sie die in Schritt 3 gewonnene Zahl durch 1000 teilen. In unserem Fall wird der Timer also alle 0,015625 s ausgelöst. 5. Multiplizieren Sie das Ergebnis mit der Anzahl der Zyklen pro Sekunde, die Sie in Schritt 2 ermittelt haben. In unserem Beispiel vergehen mit jedem Taktintervall 43.656.250 Zyklen. 6. Da jede Quantumseinheit einem Drittel Taktintervall entspricht, dividieren Sie als Nächstes die Anzahl der Zyklen durch 3. Dadurch erhalten Sie den Wert 14.528.083, was der Hexadezimalzahl 0xDE0C13 entspricht. Dies ist die Anzahl der Taktzyklen, die eine Quantumseinheit auf einem System mit 2794 Mhz und einem Taktintervall von 15,6 ms umfasst. 7. Um die Berechnung zu überprüfen, geben Sie den Wert von KiCyclesPerClockQuantum auf Ihrem System aus. Er sollte übereinstimmen (oder aufgrund von Rundungsfehlern zumindest nah genug sein). lkd> dd nt!KiCyclesPerClockQuantum L1 8149755c 00de0c10 Das Quantum steuern Sie können das Threadquantum für sämtliche Prozesse ändern, wobei Sie allerdings nur die Wahl zwischen zwei Möglichkeiten haben: kurzes Quantum (zwei Ticks, was der Standardwert für Clientcomputer ist) und langes Quantum (zwölf Ticks, der Standard für Serversysteme). In einem Jobobjekt auf einem System mit langem Quantum können Sie auch andere Quantumswerte für die Prozesse in dem Job auswählen. Um diese Einstellung zu ändern, müssen Sie auf dem Desktop oder im Windows-Explorer auf das Symbol Dieser PC rechtsklicken, Eigenschaften wählen, auf Erweiterte Systemeinstellungen klicken, die Registerkarte Erweitert aufrufen, auf die Schaltfläche Einstellungen im Abschnitt Leistung klicken und dann wiederum Erweitert aufrufen. Abb. 4–10 zeigt das resultierende Dialogfeld. Threadplanung Kapitel 4 261
Abbildung 4–10 Einstellung des Quantums im Dialogfeld In diesem Dialogfeld haben Sie die beiden folgenden Wahlmöglichkeiten: QQ Programme Diese Einstellung schreibt die Verwendung kurzer, variabler Quanten vor. Dies ist die Standardeinstellung für die Clientversionen von Windows (und clientartige Versionen wie Mobilgeräte, die Xbox, die HoloLens usw.). Wenn Sie Terminaldienste auf einem Serversystem installieren und den Rechner als Anwendungsserver einrichten, wird ebenfalls diese Einstellung ausgewählt, damit die Benutzer auf dem Terminalserver die gleichen Quantumseinstellungen haben, die auf ihrem Desktop- oder Clientsystem laufen. Sie können diese Einstellung auch manuell wählen, wenn Sie Windows Server als Desktopbetriebssystem ausführen. QQ Hintergrunddienste Bei dieser Einstellung wird das lange, feste Quantum verwendet. Dies ist die Standardeinstellung für Serversysteme. Auf einer Arbeitsstation gibt es nur einen Grund dafür, diese Option zu wählen, nämlich wenn Sie sie als Serversystem verwenden wollen. Da Änderungen an dieser Option sofort in Kraft treten, kann es jedoch auch sinnvoll sein, sie zu verwenden, wenn der Computer eine Hintergrund- oder serverartige Arbeitslast ausführen soll. Wenn beispielsweise eine lang andauernde Berechnung, Verschlüsselung oder Modellsimulation über Nacht laufen soll, können Sie am Abend die Option Hintergrunddienste wählen und am Morgen zu Programme zurückkehren. Variable Quanten Werden variable Quanten verwendet, so wird die Tabelle für variable Quanten (PspVariableQuantums), die ein Array aus sechs Quantenzahlen enthält, in die Tabelle PspForegroundQuantum (ein Array mit drei Elementen) geladen, die von der Funktion PspComputeQuantum genutzt wird. 262 Kapitel 4 Threads
Deren Algorithmus wählt den angemessenen Quantumsindex danach aus, ob es sich bei dem Prozess um einen Vordergrundprozess handelt, also ob er den Thread enthält, dem das Vordergrundfenster auf dem Desktop gehört. Ist das nicht der Fall, so wird der Index 0 gewählt, der zu dem zuvor beschriebenen Threadquantum gehört. Bei einem Vordergrundprozess dagegen wird der Quantumsindex entsprechend der Prioritätsaufteilung gewählt. Der Prioritätsaufteilungswert bestimmt die Prioritätserhöhung (siehe den entsprechenden Abschnitt weiter hinten), die der Scheduler auf Vordergrundthreads anwendet. Mit dieser Erhöhung ist auch eine entsprechende Erweiterung des Quantums verbunden. Für jede zusätzliche Prioritätsstufe (bis zu zwei) erhält der Thread ein weiteres Quantum. Wird der Thread beispielsweise um eine Prioritätsstufe erhöht, so erhält er dabei auch ein zusätzliches Quantum. Standardmäßig wendet Windows auf Vordergrundthreads die maximal mögliche Prioritätserhöhung an. Dabei beträgt die Prioritätsaufteilung 2, weshalb in der Tabelle für variable Quanten der Index 2 ausgewählt wird. Das wiederum führt dazu, dass der Thread zwei zusätzliche Quanten erhält, insgesamt also drei Quanten zur Verfügung hat. Tabelle 4–2 zeigt die genauen Quantumswerte (in Einheiten von einem Drittel Tick), die bei den verschiedenen Quantumsindizes und Quantumskonfigurationen ausgewählt werden. Index bei kurzem Quantum Index bei langem Quantum Variabel 6 12 18 12 24 36 Fest 18 18 18 36 36 36 Tabelle 4–2 Quantumswerte Wird auf einem Clientsystem ein Fenster in den Vordergrund gebracht, so werden die Quanten aller Threads in dem Prozess, der den Thread mit dem Vordergrundfenster enthält, verdreifacht. Threads im Vordergrundprozess laufen mit einem Quantum von sechs Ticks, Threads in anderen Prozessen mit dem Client-Standardquantum von zwei Ticks. Wenn Sie von einem CPU-intensiven Prozess zu einem anderen umschalten, erhält der neue Vordergrundprozess einen höheren Anteil der CPU, da seine Threads länger ausgeführt werden als die Hintergrundthreads (wobei auch hier wieder vorausgesetzt ist, dass die Vordergrund- und Hintergrundprozesse die gleiche Threadpriorität aufweisen). Der Registrierungswert für die Quantumseinstellungen Das zuvor beschriebene Dialogfeld zur Änderung der Quantumseinstellungen ändert den Registrierungswert Win32PrioritySeparation im Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\ PriorityControl. Dieser Wert gibt jedoch nicht nur die relative Länge des Threadquantums an (kurz oder lang), sondern legt auch fest, ob variable Quanten verwendet werden sollen, und bestimmt die Prioritätsaufteilung (die bei variablen Quanten zur Auswahl des Quantumsindex herangezogen wird). Der Wert setzt sich aus sechs Bits in drei 2-Bit-Feldern zusammen (siehe Abb. 4–11). Threadplanung Kapitel 4 263
kurz/lang variabel/fest Prioritäts­ aufteilung Abbildung 4–11 Die Felder des Registrierungswerts Win32PrioritySeparation Die Felder in Abb. 4–11 werden wie folgt verwendet: QQ Kurz/lang Der Wert 1 steht für lange Quanten, der Wert 2 für kurze. Die Einstellungen 0 und 3 besagen, dass der passende Standard für das System verwendet werden soll (kurze Quanten für Clientsysteme, lange für Serversysteme). QQ Variabel/fest Der Wert 1 bedeutet, dass die Tabelle für variable Quanten mit dem Algorithmus aus dem Abschnitt »Variable Quanten« verwendet werden soll. Die Einstellungen 0 und 3 besagen, dass der passende Standard für das System verwendet werden soll (variable Quanten für Clientsysteme, feste für Serversysteme). QQ Prioritätsaufteilung Dieses Feld (das in der Kernelvariablen PsPrioritySeparation gespeichert wird) legt die Prioritätsaufteilung (bis zu 2) fest (siehe den Abschnitt »Variable Quanten«). Im Dialogfeld Leistungsoptionen (siehe Abb. 4–10) können Sie nur zwischen den beiden folgenden Kombinationen wählen: kurze Quanten mit verdreifachtem Vordergrundquantum oder lange Quanten ohne Quantumsänderung für Vordergrundthreads. Durch direkte Bearbeitung des Registrierungswerts Win32PrioritySeparation können Sie jedoch auch andere Kombina­ tionen auswählen. Threads von Prozessen mit der Prozessprioritätsklasse Leerlauf erhalten stets ein einziges Threadquantum. Jegliche Quantumseinstellungen – ob standardmäßig oder in der Registrierung festgelegt – werden registriert. Auf Windows Server-Systemen, die als Anwendungsserver eingerichtet sind, beträgt der Anfangswert von Win32PrioritySeparation 26 (hexadezimal), was identisch mit dem Wert ist, den Sie durch die Option Programme im Dialogfeld Leistungsoptionen festlegen. Dadurch wird ein Quantumserweiterungs- und Prioritätserhöhungsverhalten wie auf Windows-Clientsystemen ausgewählt, was für Server, auf denen hauptsächlich Benutzeranwendungen liegen, passend ist. Auf Windows-Clientsystemen und auf Servern, die nicht als Anwendungsserver eingerichtet sind, beträgt der Anfangswert von Win32PrioritySeparation 2, was einen Wert von 0 für die Bitfelder für kurze oder lange und variable oder feste Quanten ergibt. Die tatsächliche Einstellung richtet sich daher nach dem Standardverhalten des Systems (also danach, ob es sich um ein Client- oder Serversystem handelt). Das Feld für die Prioritätsaufteilung dagegen bekommt den Wert 2. Nachdem der Registrierungswert über das Dialogfeld Leistungsoptionen geändert wurde, können Sie den Originalwert nur noch durch eine direkte Bearbeitung der Registrierung wiederherstellen. 264 Kapitel 4 Threads
Experiment: Auswirkungen einer Änderung der Quantumskonfiguration Mit einem lokalen Kerneldebugger können Sie sich ansehen, wie sich die beiden Quantumskonfigurationseinstellungen Programme und Hintergrunddienste auf die Tabellen PsPrioritySeparation und PspForegroundQuantum und auf den QuantumReset-Wert der Threads auf dem System auswirken. Gehen Sie dazu wie folgt vor: 1. Öffnen Sie in der Systemsteuerung das Programm System oder rechtsklicken Sie auf dem Desktop auf Dieser PC und wählen Sie Eigenschaften. 2. Klicken Sie auf Erweiterte Systemeinstellungen, rufen Sie die Registerkarte Erweitert auf, klicken Sie im Abschnitt Leistung auf Einstellungen und rufen Sie in dem daraufhin eingeblendeten Dialogfeld wiederum die Registerkarte Erweitert auf. 3. Aktivieren Sie die Option Programme und klicken Sie auf Übernehmen. Halten Sie das Dialogfeld während des gesamten Experiments geöffnet. 4. Geben Sie die Werte von PsPrioritySeparation und PspForegroundQuantum wie hier gezeigt aus. Wie Sie sehen, werden kurze Quanten und die Tabelle für variable Quanten verwendet und Vordergrundanwendungen mit einer Prioritätserhöhung von 2 versehen: lkd> dd nt!PsPrioritySeparation L1 fffff803'75e0e388 00000002 lkd> db nt!PspForegroundQuantum L3 fffff803'76189d28 06 0c 12 5. Schauen Sie sich den QuantumReset-Wert eines beliebigen Prozesses auf Ihrem System an. Wie bereits erklärt, ist dies das vollständige Standardquantum für jeden neu erstellten Thread auf dem System. Er wird in jedem Thread des Prozesses zwischengespeichert, aber es ist einfacher, ihn in der KPROCESS-Struktur einzusehen. In diesem Fall beträgt er 6, da das Quantum bei WinDbg wie bei den meisten anderen Anwendungen im ersten Eintrag der Tabelle PspForegroundQuantum festgelegt wird: lkd> .process Implicit process is now ffffe001'4f51f080 lkd> dt nt!_KPROCESS ffffe001'4f51f080 QuantumReset +0x1bd QuantumReset : 6 '' 6. Ändern Sie die Leistungsoption in dem Dialogfeld aus Schritt 1 und 2 auf Hintergrunddienste. 7. Geben Sie erneut die in Schritt 4 und 5 gezeigten Befehle ein. Jetzt haben sich die Werte wie in diesem Abschnitt beschrieben geändert: lkd> dd nt!PsPrioritySeparation L1 fffff803'75e0e388 00000000 → Threadplanung Kapitel 4 265
lkd> db nt!PspForegroundQuantum L3 fffff803'76189d28 24 24 24 lkd> dt nt!_KPROCESS ffffe001'4f51f080 QuantumReset +0x1bd QuantumReset : 36 '$' Prioritätserhöhung Der Windows-Scheduler passt die aktuelle (dynamische) Priorität von Threads regelmäßig durch einen internen Mechanismus zur Prioritätserhöhung an. Oft macht er das, um Latenzen zu verringern (das heißt, damit Threads schneller auf die Ereignisse reagieren, auf die sie warten) und um die Reaktivität zu erhöhen. In anderen Fällen dienen Erhöhungen dazu, Prioritätsumkehrungen und Aushungern zu vermeiden. In diesem Abschnitt beschreiben wir unter anderem die folgenden Situationen: QQ Erhöhung aufgrund von Scheduler-/Dispatcherereignissen (zur Latenzverringerung) QQ Erhöhung nach einem E/A-Abschluss (zur Latenzverringerung) QQ Erhöhung aufgrund von Eingaben von der Benutzeroberfläche (zur Latenzverringerung und Reaktivitätssteigerung) QQ Erhöhung, weil ein Thread zu lange auf eine Exekutivressource (ERESOURCE) wartet (Vermeidung des Aushungerns) QQ Erhöhung, wenn ein ausführungsbereiter Thread schon einige Zeit lang nicht ausgeführt wurde (Vermeidung von Aushungern und Prioritätsumkehrung) Wie alle Planungsalgorithmen sind auch diese Vorgehensweisen nicht perfekt und nicht für alle Anwendungen von Nutzen. Windows erhöht niemals die Priorität von Threads im Echtzeitbereich (16 bis 31). Daher ist die Planung für andere Threads in diesem Bereich stets vorhersehbar. Es wird vorausgesetzt, dass Sie bei der Verwendung von Echtzeitprioritäten wissen, was Sie tun. Windows-Clientversionen verfügen auch über einen Pseudo-Erhöhungsmechanismus, der bei der Multimediawiedergabe verwendet wird. Anders als andere Prioritätserhöhungen wird diese Art durch den Kernelmodustreiber Mmcss.sys (Multimedia Class Scheduler Service) verwaltet. Dabei treten jedoch keine echten Erhöhungen auf. Stattdessen legt der Treiber nach Bedarf neue Prioritäten für die Threads fest. Die Regeln für Prioritätserhöhungen finden dabei keine Anwendung. Im Folgenden beschreiben wir zunächst die regulären Prioritätserhöhungen durch den Kernel und erst danach die »Erhöhungen«, die MMCSS vornimmt. 266 Kapitel 4 Threads
Erhöhung aufgrund von Scheduler-/Dispatcherereignissen Beim Auftreten eines Dispatchereignisses wird die Routine KiExitDispatcher aufgerufen. Ihre Aufgabe besteht darin, die Liste der Threads im Status Verzögert bereit aufzurufen, wozu sie KiProcessThreadWaitList aufruft. Anschließend prüft sie mit KzCheckForThreadDispatch, ob irgendwelche Threads auf dem aktuellen Prozessor nicht eingeplant werden sollten. Bei einem solchen Ereignis kann der Aufrufer auch angeben, welche Art von Erhöhung auf den Threads angewandt werden soll und welches Prioritätsinkrement dabei zu verwenden ist. Die folgenden Situationen werden als AdjustUnwait-Dispatchereignisse betrachtet, da dabei ein Dispatcher-(Synchronisierungs-)Objekt in einen signalisierten Status eintritt, was dazu führen kann, dass ein oder mehrere Threads aufwachen: QQ Ein asynchroner Prozeduraufruf (APC; siehe Kapitel 6 sowie Kapitel 8 in Band 2) wird in die Warteschlange für einen Thread gestellt. QQ Ein Ereignis wird festgelegt oder gepulst. QQ Ein Timer wurde gesetzt oder die Systemzeit wurde geändert, sodass die Timer zurückgesetzt werden mussten. QQ Ein Mutex wurde freigegeben oder aufgegeben. QQ Ein Prozess wurde beendet. QQ Es wurde ein Eintrag in eine Warteschlange (KQUEUE) aufgenommen oder die Warteschlange wurde geleert. QQ Ein Semaphor wurde freigegeben. QQ Ein Thread wurde alarmiert, angehalten, wieder aufgenommen, eingefroren oder aufgetaut. QQ Ein primärer UMS-Thread wartet auf die Umschaltung zu einem geplanten UMS-Thread. Bei Planungsereignissen im Zusammenhang mit einer öffentlichen API (wie SetEvent) wird das Erhöhungsinkrement vom Aufrufer angegeben. In Windows werden Entwicklern bestimmte Werte empfohlen, die wir weiter hinten beschreiben. Bei Alarmen wird eine Erhöhung um 2 angewandt (es sei denn, der Thread wurde durch einen Aufruf von KeAlertThreadByThreadId in einen Alarmwartezustand versetzt; in diesem Fall beträgt die Erhöhung 1), da die Alarm-API nicht über Parameter verfügt, in denen der Aufrufer ein eigenes Inkrement festlegen kann. Der Scheduler hat auch zwei besondere AdjustBoost-Dispatchereignisse, die zum Sperrbesitz-Prioritätsmechanismus gehören. Damit sollen Situationen korrigiert werden, in denen ein Aufrufer, der eine Sperre der Priorität x besitzt, die Sperre für einen wartenden Thread mit einer Priorität kleiner oder gleich x freigibt. In diesem Fall muss der neue Besitzerthread warten, bis er an der Reihe ist (wenn er mit Priorität x ausgeführt wird), oder er kommt gar nicht erst ans Laufen (bei einer Priorität kleiner x). Daher fährt der freigebende Thread mit der Ausführung fort, obwohl er den neuen Besitzerthread aufwecken und ihm die Kontrolle über den Prozessor überlassen sollte. Die beiden folgenden Ereignisse rufen einen AdjustBoost-Dispatcherausstieg hervor: Threadplanung Kapitel 4 267
QQ Mit der Schnittstelle KeSetEventBoostPriority, die von der Lese/Schreib-Kernelsperre ERESOURCE verwendet wird, wird ein Ereignis festgelegt. QQ Mit der Schnittstelle KeSignalGateInterface, die beim Freigeben einer Gatesperre von verschiedenen internen Mechanismen verwendet wird, wird ein Gate festgelegt. Unwait-Erhöhung Mit Unwait-Erhöhungen wird versucht, die Latenz zwischen zwei Threads zu verringern, von denen der eine aufwacht, weil ein Objekt signalisiert wird (weshalb er in den Zustand Bereit übergeht), und der andere mit der Ausführung beginnt, um das Ende des Wartevorgangs zu verarbeiten (weshalb er in den Zustand Wird ausgeführt) übergeht. Allgemein ist es wünschenswert, dass der Thread, der aus dem Wartezustand aufwacht, so bald wie möglich ausgeführt werden kann. In den Windows-Headerdateien sind empfohlene Werte angegeben, die Kernelmodusaufrufer von APIs wie KeReleaseMutex, KeSetEvent und KeReleaseSemaphore verwenden sollten. Diese Werte entsprechen Definitionen wie MUTANT_INCREMENT, SEMAPHORE_INCREMENT und EVENT_ INCREMENT. Diese drei Definitionen sind in den Headern immer auf 1 gesetzt, weshalb wir davon ausgehen können, dass das Ende eines Wartevorgangs auf diese Objekte meistens zu einer Erhöhung um 1 führt. In der Benutzermodus-API kann kein Inkrement angegeben werden, und auch native Systemaufrufe wie NtSetEvent verfügen nicht über die Parameter zur Festlegung einer Erhöhung. Wenn diese APIs die zugrunde liegende Ke-Schnittstelle aufrufen, verwenden sie stattdessen automatisch die _INCREMENT-Standarddefinition. Das ist auch der Fall, wenn Mutexe aufgegeben oder Timer aufgrund einer Systemzeitänderung zurückgesetzt werden: Das System verwendet die Standarderhöhung, die normalerweise bei der Freigabe des Mutexes angewandt worden wäre. Die APC-Erhöhung liegt vollständig in der Verantwortung des Aufrufers. Sie werden in Kürze noch eine besondere Anwendung der APC-Erhöhung im Zusammenhang mit einem E/A-Abschluss sehen. Mit einigen Dispatcherereignissen sind keine Erhöhungen verbunden. Beispielsweise findet keine Erhöhung statt, wenn ein Timer gestellt wird oder abläuft oder wenn ein Prozess signalisiert wird. Mit all diesen Erhöhungen um 1 soll das ursprüngliche Problem gelöst werden, wobei vorausgesetzt wird, dass sowohl der freigebende als auch der wartende Thread mit derselben Priorität ausgeführt wird. Durch die Erhöhung der Priorität des wartenden Threads um eine Stufe wird dafür gesorgt, dass der wartende Thread den freigebenden Thread unterbricht, sobald die Operation abgeschlossen ist. Doch leider bringt diese Erhöhung auf Einprozessorsystemen nicht viel, wenn die Voraussetzung nicht erfüllt ist. Wenn beispielsweise der wartende Thread die Priorität 4 und der freigebende die Priorität 8 hat, dann führt eine Erhöhung auf 5 nicht dazu, dass die Latenz verringert und die vorzeitige Unterbrechung erzwungen wird. Auf Mehrprozessorsystemen dagegen kann der Thread mit erhöhter Priorität aufgrund der Übernahme- und Ausgleichsalgorithmen eine bessere Chance haben, von einem anderen logischen Prozessor aufgegriffen zu werden. Der Grund dafür ist eine Designentscheidung bei der ursprünglichen 268 Kapitel 4 Threads
NT-Architektur, durch die der Besitz von Sperren bis auf wenige Ausnahmen nicht verfolgt wird. Der Scheduler kann also nicht sicher sein, wer nun wirklich der Besitzer eines Ereignisses ist und ob es tatsächlich als Sperre verwendet wird. Selbst bei einer Verfolgung des Sperrbesitzes wird (zur Vermeidung von Konvoiproblemen) die Besitzinformation nicht übergeben (außer bei Exekutivressourcen, worüber es weiter hinten noch gehen wird). Für bestimmte Arten von Sperrobjekten, die Ereignisse oder Gates als Synchronisierungsobjekte verwenden, kann das Problem durch eine Sperrbesitzerhöhung gelöst werden. Auf Mehrprozessorsystemen kann der ausführungsbereite Thread außerdem aufgrund der weiter hinten erläuterten Prozessorverteilungs- und Lastenausgleichverfahren von einem anderen Prozessor aufgegriffen werden, wobei seine erhöhte Priorität die Wahrscheinlichkeit für die Ausführung auf diesem zweiten Prozessor erhöht. Sperrbesitzerhöhung Da Sperren für Exekutivressourcen (ERESOURCE) und kritische Abschnitte zugrunde liegende Dispatcherobjekte verwenden, kann eine Freigabe dieser Sperren zu einer Unwait-Erhöhung wie im vorherigen Abschnitt führen. Da die Implementierung dieser Objekte Informationen über den Besitzer der Sperre verfolgt, kann der Kernel jedoch auch fundiertere Entscheidung darüber fällen, welche Art von Erhöhung bei AdjustBoost angewandt werden soll. Dabei wird AdjustIncrement auf die aktuelle Priorität des Threads gesetzt, der die Sperre freigibt (oder einrichtet), abzüglich jeglicher Erhöhungen aufgrund der Prioritätsaufteilung für den GUI-Vordergrund. Außerdem wird vor dem Aufruf von KiExitDispatcher die Funktion KiRemoveBoostThread vom Ereignis- und Gatecode aufgerufen, um den freigebenden Thread wieder auf seine reguläre Priorität zurückzusetzen. Das ist notwendig, um einen Sperrenkonvoi zu vermeiden, bei dem zwei Threads, die sich ständig gegenseitig eine Sperre übergeben, eine immer stärkere Erhöhung bekommen. Pushlocks sind unfaire Sperren, da der Sperrbesitz in einem umkämpften Erwerbspfad nicht vorhersehbar ist (sondern wie bei einem Spinlock zufällig). Bei ihnen werden keine Prioritätserhöhungen aufgrund des Sperrbesitzes angewendet, denn das würde nur vorzeitige Unterbrechungen und eine Ausuferung der Prioritäten fördern. Es ist auch nicht nötig, denn die Sperre kommt nach der Freigabe sofort frei, weil der normale Pfad mit Wartezustand/Beendigung des Wartezustands umgangen wird. Zwischen Sperrbesitz- und Unwait-Erhöhung gibt es noch weitere Unterschiede in der Art, wie der Scheduler die Erhöhung anwendet. Das ist Thema des nächsten Abschnitts. Erhöhung nach E/A-Abschluss Nach Abschluss bestimmter E/A-Operationen wendet Windows vorübergehende Prioritätserhöhungen an, damit Threads, die auf die E/A gewartet haben, eine größere Chance haben, sofort ausgeführt zu werden und das zu verarbeiten, auf das sie gewartet haben. In den Headerdateien des Windows Driver Kit (WDK) finden Sie zwar empfohlene Erhöhungswerte Threadplanung Kapitel 4 269
(suchen Sie dazu in Wdm.h oder Ntddk.h nach #define IO_; siehe auch Tabelle 4–3), doch den tatsächlichen Wert für die Erhöhung vergibt der Gerätetreiber, wenn er mit seinem Aufruf der Kernelfunktion IoCompleteRequest eine E/A-Anforderung abschließt. Wie Sie in Tabelle 4–3 sehen, haben E/A-Anforderungen an Geräte mit höherer Reaktivität einen größeren Erhöhungswert. Gerät Erhöhung Festplatte, CD-ROM, parallele Schnittstelle, Video 1 Netzwerk, Mailslot, benannte Pipes, serielle Schnittstelle 2 Tastatur, Maus 6 Soundkarte 8 Tabelle 4–3 Empfohlene Erhöhungswerte Rein intuitiv erwartet man von seiner Videokarte oder Festplatte eine bessere Reaktivität, als eine Erhöhung von 1 bringen kann. Allerdings versucht der Kernel, die Latenz zu optimieren, auf die einige Geräte (und menschliche Sinne) empfindlicher reagieren als andere. Beispielsweise erwartet die Soundkarte etwa jede Millisekunde Daten, um Musik ohne wahrnehmbare Aussetzer abspielen zu können, wohingegen eine Videokarte nur eine Ausgabe von 24 Einzelbildern pro Sekunde leistet und daher nur alle 40 ms Daten benötigt, bevor Unregelmäßigkeiten zu erkennen sind. Wie bereits angedeutet, beruhen diese E/A-Abschlusserhöhungen auf den zuvor beschriebenen Unwait-Erhöhungen. In Kapitel 6 wird der E/A-Abschlussmechanismus ausführlich beschrieben. Für unsere jetzige Erörterung ist wichtig, dass der Kernel den Signalisierungscode in der API IoCompleteRequest entweder durch einen APC (für asynchrone E/A) oder durch ein Ereignis (für synchrone E/A) implementiert. Wenn ein Treiber beispielsweise IO_DISK_INCREMENT für einen asynchronen Lesevorgang auf der Festplatte an IoCompleteRequest übergibt, ruft der Kernel KeInsertQueueApc mit IO_DISK_INCREMENT als Boostparameter auf. Wird der Wartezustand des Threads aufgrund des APCs unterbrochen, erhält er schließlich die Erhöhung um 1. Beachten Sie, dass die Erhöhungswerte aus Tabelle 4–3 nur Empfehlungen von Microsoft darstellen. Treiberentwickler können sie auch ignorieren, und für besondere Treiber können eigene Werte benutzt werden. Beispielsweise wird für einen Treiber, der Ultraschalldaten eines medizinischen Instruments verarbeiten und eine Visualisierungsanwendung im Benutzermodus über neue Daten benachrichtigen muss, auch einen Erhöhungswert von 8 verwendet, um die gleiche Latenz zu erreichen wie eine Soundkarte. Aufgrund der Art und Weise, in der Windows-Treiberstacks aufgebaut sind (siehe Kapitel 6), schreiben Treiberentwickler jedoch meistens Minitreiber, die einen Microsoft-Treiber mit einem eigenen Erhöhungswert für IoCompleteRequest aufrufen. Beispielsweise rufen Treiber für RAID- und SATA-Controllerkarten gewöhnlich StorPortCompleteRequest auf, um die Verarbeitung ihrer Anforderungen abzuschließen. Dieser Aufruf hat keinen Parameter für einen Erhöhungswert, da der Treiber Storport.sys den richtigen Wert beim Aufruf des Kernels einfügt. Außerdem wird jedes Mal, 270 Kapitel 4 Threads
wenn ein Dateisystemtreiber (der an dem Gerätetyp FILE_DEVICE_DISK_FILE_SYSTEM oder FILE_­ DEVICE_NETWORK_FILE_SYSTEM zu erkennen ist) seine Anforderung abschließt, eine Erhöhung um IO_DISK_INCREMENT angewendet, falls der Treiber stattdessen IO_NO_INCREMENT (0) übergeben hat. Dadurch ist dieser Erhöhungswert von einer Empfehlung mehr oder weniger zu einer Anforderung geworden, die der Kernel durchsetzt. Erhöhung durch Warten auf Exekutivressourcen Wenn ein Thread versucht, eine Exekutivressource zu erwerben (ERESOURCE; siehe Kapitel 8 in Band 2), die sich bereits im exklusiven Besitz eines anderen Threads befindet, muss er in den Wartezustand übergeben, bis der andere Thread die Ressource freigibt. Um die Gefahr von Deadlocks zu verringern, führt die Exekutive diese Wartevorgänge in Intervallen von 500 ms durch, anstatt endlos auf die Ressource zu warten. Ist die Ressource nach diesen 500 ms immer noch im Besitz des anderen Threads, versucht die Exekutive, ein Aushungern der CPU zu verhindern, indem sie die Dispatchersperre erwirbt und die Priorität der Besitzerthreads auf 15 erhöht (falls die ursprüngliche Priorität des wartenden Threads nicht bereits 15 beträgt), ihre Quanten zurücksetzt und einen weiteren Wartevorgang durchführt. Exekutivressourcen können entweder gemeinsam oder exklusiv genutzt werden. Der Kernel versucht zuerst, die Priorität des exklusiven Besitzers zu erhöhen, und prüft dann, ob es gemeinsame Besitzer gibt, um deren Prioritäten zu erhöhen. Wenn der wartende Thread wieder in den Wartezustand eintritt, besteht die Hoffnung, dass der Scheduler einen der Besitzerthreads einplant, der dann genug Zeit hat, um seine Arbeit abzuschließen und die Ressource freizugeben. Dieser Erhöhungsmechanismus wird nur dann verwendet, wenn das Flag Disable Boost für die Ressource nicht gesetzt ist. Entwickler können dieses Flag setzen, wenn der hier beschriebene Prioritätsumkehrungsmechanismus bei ihrer Art der Ressourcenverwendung gut funktioniert. Dieser Mechanismus ist nicht perfekt. Hat die Ressource beispielsweise mehrere gemeinsame Besitzer, erhöht die Exekutive die Prioritäten all dieser Threads auf 15, was zu einer plötzlichen Spitze von Threads hoher Priorität auf dem System führt, die obendrein noch alle ein vollständiges Quantum haben. Der ursprüngliche Besitzerthread wird zwar als erster ausgeführt (da er der erste war, dessen Priorität erhöht wurde, und er daher am Anfang der Bereitschaftsliste steht), doch die anderen gemeinsamen Besitzer folgen als Nächstes, da die Priorität des Wartethreads nicht erhöht wurde. Erst nachdem alle gemeinsamen Besitzer die Gelegenheit zur Ausführung erhalten haben und ihre Priorität unter die des Wartethreads abgesenkt wurde, erhält Letzterer endlich die Chance, die Ressource zu erwerben. Da gemeinsame Besitzer ihren gemeinsamen in einen exklusiven Besitz umwandeln können, sobald der exklusive Besitzer die Ressource freigegeben hat, kann es sein, dass der Mechanismus nicht wie vorgesehen funk­ tioniert. Prioritätserhöhung für Vordergrundthreads nach Wartevorgängen Wenn ein Thread in einem Vordergrundprozess einen Wartevorgang auf ein Kernelobjekt abschließt, erhöht der Kernel die aktuelle Priorität (nicht die Basispriorität) dieses Threads um den aktuellen Wert von PsPrioritySeparation (wobei das Fenstersystem bestimmt, welcher Prozess Threadplanung Kapitel 4 271
als Vordergrundprozess angesehen wird). Wie zuvor in diesem Kapitel im Abschnitt »Das Quantum steuern« erklärt, spiegelt PsPrioritySeparation den Quantumstabellenindex wider, der zur Auswahl des Quantums für die Threads von Vordergrundanwendungen herangezogen wird. In diesem Fall dient er jedoch als Wert für die Prioritätserhöhung. Diese Erhöhung wird vorgenommen, um die Reaktivität interaktiver Anwendungen zu verbessern. Durch die kleine Prioritätserhöhung nach dem Abschluss eines Wartevorgangs hat die Vordergrundanwendung eine bessere Chance, sofort ausgeführt zu werden, vor allem dann, wenn im Hintergrund weitere Prozesse mit der gleichen Basispriorität laufen. Experiment: Erhöhungen und Verringerungen der Vordergrundpriorität beobachten Mit dem Werkzeug CPU Stress können Sie Prioritätserhöhungen in Aktion beobachten. Gehen Sie dazu wie folgt vor: 1. Öffnen Sie in der Systemsteuerung das Programm System oder rechtsklicken Sie auf dem Desktop auf Dieser PC und wählen Sie Eigenschaften. 2. Klicken Sie auf Erweiterte Systemeinstellungen, rufen Sie die Registerkarte Erweitert auf, klicken Sie im Abschnitt Leistung auf Einstellungen und rufen Sie in dem daraufhin eingeblendeten Dialogfeld wiederum die Registerkarte Erweitert auf. 3. Aktivieren Sie die Option Programme. Dadurch erhält PsPrioritySeparation den Wert 2. 4. Starten Sie CPU Stress, rechtsklicken Sie auf Thread 1 und wählen Sie Busy aus dem Kontextmenü. 5. Starten Sie die Leistungsüberwachung. 6. Klicken Sie in der Symbolleiste auf Hinzufügen oder drücken Sie (Strg) + (I), um das Dialogfeld Leistungsindikatoren hinzufügen zu öffnen. 7. Wählen Sie das Leistungsobjekt Thread und darunter den Indikator Aktuelle Priorität aus. 8. Wählen Sie im Feld Instanzen den Eintrag <Alle Instanzen> und klicken Sie auf Suchen. → 272 Kapitel 4 Threads
9. Scrollen Sie zum Prozess CPUSTRES, markieren Sie den zweiten Thread (das ist Thread 1; beim ersten Thread handelt es sich um den GUI-Thread) und klicken Sie auf Hinzufügen. Jetzt sehen Sie Folgendes: 10. Klicken Sie auf OK. 11. Rechtsklicken Sie auf den Indikator und wählen Sie Eigenschaften. 12. Klicken Sie auf die Registerkarte Grafik und ändern Sie den Maximalwert für die vertikale Skalierung auf 16. Klicken Sie auf OK. → Threadplanung Kapitel 4 273
13. Bringen Sie den Prozess CPUSTRES in den Vordergrund. Dabei können Sie beobachten, wie die Priorität des Threads CPUSTRES um 2 erhöht wird und dann auf die Basispriorität zurückfällt. CPUSTRES erhält regelmäßig eine Erhöhung um 2, da der Thread, den Sie beobachten, ungefähr 25 % der Zeit schläft und danach aufwacht (das ist es, was der Aktivitätsgrad Busy bedeutet). Die Erhöhung wird angewendet, wenn der Thread aufwacht. Wenn Sie den Aktivitätsgrad auf Maximum setzen, sind dagegen keine Erhöhungen zu sehen, da der Thread dabei in eine Endlosschleife gestellt wird, keine Wartefunktionen mehr aufruft und deshalb keine Erhöhungen mehr erhält. 14. Beenden Sie die Leistungsüberwachung und CPU Stress. Prioritätserhöhung nach dem Aufwachen von GUI-Threads Threads, die Fenster besitzen, erhalten eine zusätzliche Erhöhung um 2, wenn sie aufgrund von Fensteraktivitäten wie dem Erhalt von Fensternachrichten aufwachen. Das Fenstersystem (Win32k.sys) wendet diese Erhöhungen an, wenn es KeSetEvent aufruft, um ein Ereignis zum Wecken eines GUI-Threads festzulegen. Für diese Erhöhung gibt es einen ähnlichen Grund wie für die vorherige: um interaktive Anwendungen zu bevorzugen. 274 Kapitel 4 Threads
Experiment: Prioritätserhöhungen von GUI-Threads beobachten Sie können sich ansehen, wie das Fenstersystem die Priorität von GUI-Threads, die zur Verarbeitung von Fensternachrichten aufwachen, um 2 erhöht, indem Sie die aktuelle Priorität einer GUI-Anwendung beobachten und dabei die Maus über das Fenster bewegen. Gehen Sie dazu wie folgt vor: 1. Öffnen Sie in der Systemsteuerung das Programm System. 2. Klicken Sie auf Erweiterte Systemeinstellungen, rufen Sie die Registerkarte Erweitert auf, klicken Sie im Abschnitt Leistung auf Einstellungen und rufen Sie in dem daraufhin eingeblendeten Dialogfeld wiederum die Registerkarte Erweitert auf. 3. Aktivieren Sie die Option Programme. Dadurch erhält PsPrioritySeparation den Wert 2. 4. Starten Sie den Editor. 5. Starten Sie die Leistungsüberwachung. 6. Klicken Sie in der Symbolleiste auf Hinzufügen oder drücken Sie (Strg) + (I), um das Dialogfeld Leistungsindikatoren hinzufügen zu öffnen. 7. Wählen Sie das Leistungsobjekt Thread und darunter den Indikator Aktuelle Priorität aus. 8. Geben Sie Notepad in das Feld Instanzen ein und klicken Sie auf Suchen. 9. Scrollen Sie zum Eintrag Notepad/0 herunter, klicken Sie darauf, klicken Sie auf Hinzufügen und dann auf OK. 10. Ändern Sie wie im vorherigen Experiment den Maximalwert für die vertikale Skalierung auf 16. Als Priorität für Thread 0 des Editors wird jetzt 8 oder 10 angezeigt. Da der Editor kurz nach dem Erhalt der Prioritätserhöhung um 2 für Vordergrundprozesse in einen Wartezustand eingetreten ist, kann es sein, dass seine Priorität noch nicht wieder von 10 auf 8 gesunken ist. 11. Bewegen Sie den Mauszeiger über das Editorfenster, während die Leistungsüberwachung im Vordergrund ist. (Sorgen Sie dafür, dass beide Fenster auf dem Bildschirm zu sehen sind.) Aus den zuvor genannten Gründen bleibt die Priorität manchmal bei 10 und fällt manchmal auf 9 ab. Es ist nicht sehr wahrscheinlich, dass Sie den Prioritätswert von 8 sehen, da der Editor nach dem Erhalt der Prioritätserhöhung für GUI-Threads um 2 nur so kurz läuft, dass die Priorität vor dem nächsten Erwachen kaum um mehr als eine Stufe sinken kann. (Dies liegt an zusätzlichen Fensteraktivitäten und der Tatsache, dass er erneut eine Erhöhung um 2 erhält.) → Threadplanung Kapitel 4 275
12. Bringen Sie den Editor in den Vordergrund. Dabei können Sie beobachten, wie die Priorität auf 12 erhöht wird und dann bei diesem Wert bleibt. Das liegt daran, dass der Thread zwei Erhöhungen erhält: einmal die Erhöhung um 2 für erwachende GUIThreads und eine weitere Erhöhung um 2, da der Editor jetzt im Vordergrund ist. (Unter Umständen können Sie auch beobachten, wie die Priorität wieder auf 11 fällt, wenn der Thread die übliche Prioritätssenkung erfährt, die bei Threads mit erhöhter Priorität am Ende des Quantums auftritt.) 13. Bewegen Sie den Mauszeiger über das Editorfenster, während es im Vordergrund ist. Bringen Sie den Editor in den Vordergrund. Dabei können Sie unter Umständen beobachten, wie die Priorität auf 11 oder möglicherweise sogar 10 fällt, wenn der Thread die übliche Prioritätssenkung erfährt, die bei Threads mit erhöhter Priorität am Ende des Quantums auftritt. Die Erhöhung um 2, die der Editor erhalten hat, weil er der Vordergrundprozess ist, bleibt jedoch so lange bestehen, wie sich der Editor im Vordergrund befindet. 14. Beenden Sie die Leistungsüberwachung und den Editor. Prioritätserhöhung gegen CPU-Aushungerung Stellen Sie sich folgende Situation vor: Ein Thread mit Priorität 7 wird ausgeführt und verhindert, dass ein Thread mit Priorität 4 jemals CPU-Zeit zugeteilt bekommt. Allerdings wartet ein Thread der Priorität 11 auf eine Ressource, die der Priorität-4-Thread gesperrt hat. Da der Priorität-7-Thread aber die gesamte CPU-Zeit verbraucht, läuft der Priorität-4-Thread nie lange genug, um das zu erledigen, was er tun muss, und die Ressource freizugeben, die den Priorität-11-Thread blockiert. Eine solche Situation wird als Prioritätsumkehrung oder Prioritätsinversion bezeichnet. Wie geht Windows mit dieser Situation um? Die ideale Lösung (zumindest in der Theorie) besteht darin, den Überblick über alle Sperren und ihre Besitzer zu bewahren und die Priorität der passenden Threads zu erhöhen, damit die Verarbeitung voranschreiten kann. Diese Idee wird in der Autoboost-Einrichtung umgesetzt, die weiter hinten in diesem Kapitel in dem gleichnamigen Abschnitt erläutert wird. Gegen Aushungerungssituationen im Allgemeinen wird jedoch die folgende Schutzmaßnahme angewendet. Sie haben bereits gesehen, dass der Code für Exekutivressourcen in einer solchen Situation die Priorität der Besitzerthreads erhöht, sodass sie die Chance bekommen, zu laufen und die Ressource freizugeben. Exekutivressourcen sind jedoch nur eine von vielen Synchronisierungsstrukturen, die Entwicklern zur Verfügung stehen, und diese Prioritätsverstärkungstechnik lässt sich auf keines der anderen Primitiva anwenden. Daher gibt es in Windows einen allgemeinen Mechanismus gegen CPU-Aushungerung, der zu einem Thread namens Ausgleichssatz-Manager gehört. (Dies ist ein Systemthread, der hauptsächlich für Speicherverwaltungsfunktionen da ist und ausführlicher in Kapitel 5 beschrieben wird.) Dieser Thread durchsucht die Bereitschaftswarteschlange jede Sekunde nach Threads, die sich seit ca. vier Sekunden im Zustand Bereit befinden (also noch nicht ausgeführt wurden). Wenn der Ausgleichssatz-Manager einen solchen Thread findet, erhöht er dessen Priorität auf 15 und setzt den Quantumssollwert ent- 276 Kapitel 4 Threads
sprechend auf drei Quantumseinheiten. Nach Ablauf dieses Quantums fällt die Priorität des Threads sofort wieder auf die ursprüngliche Basispriorität zurück. Hat der Thread seine Arbeit noch nicht beendet und steht ein Thread höherer Priorität zur Ausführung bereit, so kehrt der Thread in die Bereitschaftswarteschlange zurück, wo er nach weiteren vier Sekunden wieder die Berechtigung für einen weiteren Prioritätsschub erhält. Um CPU-Zeit zu sparen, untersucht der Ausgleichssatz-Manager jedoch nicht jedes Mal sämtliche ausführungsbereiten Threads, sondern immer nur 16. Gibt es mehr Threads auf derselben Prioritätsstufe, merkt er sich, wo er aufgehört hat, und fährt beim nächsten Durchgang dort fort. Außerdem verstärkt er in jedem Durchgang nur die Priorität von höchsten zehn Threads. Sind mehr als zehn für einen solchen Prioritätsschub berechtigt (was schon auf ein ungewöhnlich stark beschäftigtes System hindeutet), so fährt er beim nächsten Durchgang mit den restlichen fort. Wir hatten zuvor betont, dass die Planungsentscheidungen in Windows nicht von der Anzahl der Threads abhängen und stets gleich viel Zeit erfordern. Da der Ausgleichssatz-Manager jedoch manuell die Bereitschaftswarteschlangen untersuchen muss, hängt die Dauer dieser Operation von der Anzahl der Threads ab: Je mehr Threads es gibt, umso mehr Zeit erfordert die Untersuchung. Allerdings gilt der Ausgleichssatz-Manager nicht als Teil des Schedulers oder dessen Algorithmen, sondern als Erweiterungsmechanismus zur Steigerung der Zuverlässigkeit. Da es überdies eine Obergrenze für die Anzahl der zu untersuchenden Threads und Warteschlangen gibt, ist die Leistungseinbuße minimal und ihr höchster Wert berechenbar. Experiment: Prioritätserhöhungen gegen CPU-Aushungerung beobachten Mit dem Werkzeug CPU Stress können Sie Prioritätserhöhungen beobachten. In diesem Experiment werden Sie sehen, wie sich die CPU-Nutzung ändert, wenn die Priorität eines Threads heraufgesetzt wird. Gehen Sie dazu wie folgt vor: 1. Führen Sie CPUStres.exe aus. 2. Der Aktivitätsgrad von Thread 1 ist Low. Ändern Sie ihn auf Maximum. 3. Die Threadpriorität von Thread 1 ist Normal. Ändern Sie ihn auf Lowest. 4. Klicken Sie auf Thread 2. Sein Aktivitätsgrad ist Low. Ändern Sie ihn auf Maximum. → Threadplanung Kapitel 4 277
5. Ändern Sie die Prozessaffinitätsmaske auf Affinität mit einem einzigen logischen Prozessor. Öffnen Sie dazu das Menü Process und wählen Sie Affinity. (Es spielt keine Rolle, welchen Prozessor Sie dabei auswählen.) Alternativ können Sie diese Änderung auch im Task-­Manager vornehmen. Das Fenster von CPU Stress sollte jetzt wie folgt aussehen: 6. Starten Sie die Leistungsüberwachung. 7. Klicken Sie in der Symbolleiste auf Leistungsindikator hinzufügen oder drücken Sie (Strg) + (I), um das Dialogfeld Leistungsindikatoren hinzufügen zu öffnen. 8. Wählen Sie das Objekt Thread und darunter den Indikator Aktuelle Priorität aus. 9. Geben Sie CPUSTRES in das Feld Instanzen ein und klicken Sie auf Suchen. 10. Wählen Sie die Threads 1 und 2 aus (Thread 0 ist der GUI-Thread), klicken Sie auf Hinzufügen und dann auf OK. 11. Ändern Sie das Maximum der vertikalen Skalierung für beide Indikatoren auf 16. 12. Da die Leistungsüberwachung nur einmal pro Sekunde aktualisiert wird, kann es sein, dass Sie die Prioritätsschübe dabei verpassen. Als Abhilfemaßnahme frieren Sie die Anzeige mit (Strg) + (F) ein und halten dann (Strg) + (U) gedrückt, um häufigere Aktualisierungen zu erzwingen. Mit etwas Glück können Sie dann eine Prioritätserhöhung des Threads niedriger Priorität auf etwa 15 sehen: → 278 Kapitel 4 Threads
13. Beenden Sie die Leistungsüberwachung und CPU Stress. Prioritätserhöhungen anwenden Bei der Beschreibung von KiExitDispatcher haben Sie gesehen, dass KiProcessThreadWait aufgerufen wird, um Threads in der Liste von Threads mit verzögerter Bereitschaft zu verarbeiten. Hier werden die vom Aufrufer übergebenen Informationen über die Prioritätserhöhung verarbeitet. Dazu werden alle Threads mit verzögerter Bereitschaft in einer Schleife durchlaufen, die Verknüpfung zu ihren Warteblöcken aufgehoben ( jedoch nur für Active- und Bypassed-Blöcke) und die beiden Schlüsselwerte AdjustReason und AdjustIncrement im Threadsteuerungsblock des Kernels gesetzt. Als Grund (AdjustReason) wird eine der beiden zuvor beschriebenen A­ djust-Möglichkeiten angegeben, während das Inkrement dem Erhöhungswert entspricht. Anschließend wird KiDeferredReady aufgerufen. Diese Funktion bereitet den Thread zur Ausführung vor, indem sie den (im Folgenden beschriebenen) Quantums- und Prioritätsauswahlalgorithmus und den Prozessorauswahlalgorithmus (beschrieben im Abschnitt »Prozessorauswahl« weiter hinten in diesem Kapitel) ausführt. Sehen wir uns zunächst den Fall an, dass der Algorithmus eine Prioritätserhöhung anwendet, was nur bei Threads geschieht, die sich nicht im Echtzeit-Prioritätsbereich befinden. Eine AdjustUnwait-Erhöhung erfolgt nur dann, wenn der Thread noch keinen ungewöhnlichen Prioritätsschub erhalten hat und wenn die Prioritätserhöhung nicht deaktiviert wurde. Letzteres kann durch Setzen des Flags DisableBoost in KTHREAD mithilfe von SetThreadPriorityBoost geschehen oder wenn der Kernel erkennt, dass der Thread sein Quantum bereits erschöpft hat (aber der Taktinterrupt nicht ausgelöst wurde, um es zu verbrauchen) und seinen Wartezustand nach weniger als zwei Ticks verlässt. Threadplanung Kapitel 4 279
Wenn keiner dieser Umstände vorliegt, wird die neue Priorität des Threads durch Addi­ tion von AdjustIncrement zur aktuellen Basispriorität des Threads bestimmt. Gehört der Thread zu einem Vordergrundprozess (ist also seine Speicherpriorität auf MEMORY_PRIORITY_FOREGROUND gesetzt, was Win32k.sys bei einer Änderung des Fokus erledigt), wird auf die neue Priorität außerdem die Erhöhung aufgrund der Prioritätsaufteilung (PsPrioritySeparation) aufgeschlagen, also der zuvor beschriebene Vordergrund-Prioritätsschub. Anschließend prüft der Kernel noch, ob die neue Priorität höher ist als die aktuelle Priorität des Threads, und begrenzt den Wert auf maximal 15, um zu verhindern, dass er in den Echtzeitbereich wechselt. Anschließend legt der Kernel den Wert als neue aktuelle Priorität des Threads fest. Wurde ein Vordergrundschub angewendet, wird dieser Wert auch in das Feld ForegroundBoost von KTHREAD eingetragen, was zu einem PriorityDecrement-Wert gleich dem Vordergrundschub führt. Bei AdjustBoost-Erhöhungen prüft der Kernel, ob die aktuelle Threadpriorität kleiner als AdjustIncrement (die Priorität des festlegenden Threads) und kleiner als 13 ist. Ist das der Fall und wurden Prioritätserhöhungen für den Thread nicht deaktiviert, wird die AdjustIncrement-­ Priorität als neue aktuelle Priorität verwendet, allerdings begrenzt auf einen Maximalwert von 13. Das KTHREAD-Feld UnusualBoost enthält den Erhöhungswert, was dazu führt, dass PriorityDecrement auf einen Wert gleich der Sperrbesitzerhöhung gesetzt wird. In allen Fällen, in denen PriorityDecrement vorhanden ist, wird das Quantum des Threads auf einen Tick auf der Grundlage des Wertes von KiLockQuantumTarget gesetzt. Dadurch wird sichergestellt, dass Vordergrund- und ungewöhnliche Erhöhungen schon nach einem statt nach den üblichen zwei Ticks (oder welcher Wert sonst eingestellt ist) wieder zurückgenommen werden (siehe den nächsten Abschnitt). Dies geschieht auch, wenn eine AdjustBoost-Erhöhung angefordert wird, der Thread aber bereits mit einer Priorität von 13 oder 14 läuft oder die Erhöhung deaktiviert ist. Nachdem diese Arbeiten erledigt sind, wird AdjustReason auf AdjustNone gesetzt. Erhöhungen zurücknehmen Die Rücknahme von Erhöhungen erfolgt in KiDeferredReadyThread während der im vorherigen Abschnitt gezeigten Prioritäts- und Quantumsneuberechnungen. Als Erstes prüft der Algorithmus die Art der Anpassung, die erfolgen soll. Im Fall von AdjustNone – was bedeutet, dass der Thread ausführungsbereit wurde, möglicherweise aufgrund einer vorzeitigen Unterbrechung – wird das Quantum des Threads neu berechnet, falls es bereits den Sollwert erreicht hat, der Taktinterrupt dies aber noch nicht bemerkt hat. (Dies gilt, wenn der Thread auf einer der dynamischen Prioritätsstufen ausgeführt wurde.) Auch die Threadpriorität wird neu berechnet. Bei AdjustUnwait und AdjustBoost (und ebenfalls außerhalb des Echtzeitbereichs) prüft der Kernel, ob der Thread sein Quantum stillschweigend erschöpft hat (wie im vorherigen Abschnitt). Wenn das der Fall ist oder wenn der Thread mit einer Basispriorität von 14 oder höher lief oder wenn kein PriorityDecrement-Wert angegeben ist und der Thread einen Wartezustand von weniger als zwei Ticks beendet hat, dann werden das Quantum und die Priorität des Threads neu berechnet. 280 Kapitel 4 Threads
Eine Neuberechnung der Priorität erfolgt nur bei Threads außerhalb des Echtzeitbereichs. Dazu werden von der aktuellen Priorität des Threads die Vordergrunderhöhung, die ungewöhnliche Erhöhung (die Kombination dieser beiden Elemente ergibt PriorityDecrement) und 1 subtrahiert. Dabei bildet die Basispriorität die Untergrenze für die neue Priorität. Jegliche vorhandenen Prioritätsdekremente werden auf null gesetzt (wodurch alle ungewöhnlichen und Vordergrunderhöhungen entfernt werden). Bei einer Sperrbesitz- oder anderen ungewöhnlichen Erhöhung geht dadurch der gesamte Erhöhungswert verloren. Bei einer regulären A­ djustUnwait-Erhöhung dagegen sinkt die Priorität langsam durch Subtraktion von 1, bis die Basispriorität als untere Grenze erreicht ist. Es gibt noch eine andere Situation, in der Erhöhungen zurückgenommen werden müssen, nämlich aufgrund der Regel für Sperrbesitzerhöhungen, nach der der festlegende Thread seine Erhöhung verlieren muss, wenn er dem aufwachenden Thread seine aktuelle Priorität gespendet hat (um einen Sperrenkonvoi zu verhindern). Dazu wird die Funktion KiRemoveBoostThread verwendet. Sie dient auch dazu, Prioritätserhöhungen aufgrund von verzögerten Prozeduraufrufen (Deferred Procedure Calls, DPCs) und gegen ERESOURCE-Sperrenaushungerung zurückzunehmen. Das einzig Besondere an dieser Routine ist, dass beim Berechnen der neuen Priorität die Trennung der ForegroundBoost- und UnusualBoost-Bestandteile von PriorityDecrement getrennt berücksichtigt werden, um GUI-Vordergrunderhöhungen aufrechtzuerhalten, die der Thread erworben hat. Diese Vorgehensweise wurde mit Windows 7 eingeführt und sorgt dafür, dass sich Threads mit Sperrbesitzerhöhung nicht unvorhersehbar verhalten, wenn sie im Vordergrund laufen, und umgekehrt. Abb. 4–12 zeigt, wie normale Erhöhungen am Ende des Quantums von einem Thread entfernt werden. Quantum Erhöhung am Ende Priorität des Wartevorgangs Prioritätsabfall am Ende des Quantums Vorzeitiges Unterbrechen (vor Ende des Quantums) Basispriorität Ausführen Warten Ausführen Umlaufverfahren auf Basispriorität Ausführen Zeit Abbildung 4–12 Erhöhung und Senkung der Priorität Threadplanung Kapitel 4 281
Prioritätserhöhung für Multimediaanwendungen und Spiele Die Prioritätserhöhungen gegen CPU-Aushungerung können zwar ausreichen, um einen Thread aus einem abnorm langen Wartezustand oder einem möglichen Deadlock herauszuholen, aber mit den Ressourcenanforderungen einer CPU-intensiven Anwendung wie Windows Media Player oder einem 3-D-Computerspiel werden sie nicht fertig. Aussetzer und andere Audiofehler haben Windows-Benutzer lange gestört. Der Benutzermodus-Audiostack von Windows hat die Situation sogar verschärft, da er noch mehr Möglichkeiten für vorzeitige Unterbrechungen bot. Als Gegenmaßnahmen verwenden Clientversionen den MMCSS-Treiber, der in %SystemRoot%\System32\Drivers\MMCSS.sys implementiert ist (und weiter vorn in diesem Kapitel beschrieben wurde). Er soll für eine fehlerfreie Multimediawiedergabe in Anwendungen sorgen, die sich bei ihm registrieren. In Windows 7 ist MMCSS als Dienst und nicht als Treiber implementiert. Das bietet jedoch eine gewisse Gefahr: Wenn der Thread zur Verwaltung von MMCSS aus irgendeinem Grund blockiert sein sollte, behalten die von ihm verwalteten Threads ihre Echtzeitpriorität, was zu einer systemweiten Aushungerung führen kann. Zur Lösung dieses Problems wurde der Code in den Kernel verschoben, wo der Verwaltungsthread (und andere von MMCSS verwendete Ressourcen) nicht beeinträchtigt werden kann. Die Implementierung als Kerneltreiber bietet noch andere Vorteile, z. B. die Verwendung eines direkten Zeigers auf das Prozess- und das Threadobjekt anstelle von IDs und Handles. Dadurch werden Suchvorgänge nach den IDs und Handles vermieden und eine schnellere Kommunikation mit dem Scheduler und dem Energie-Manager ermöglicht. Clientanwendungen können sich bei MMCSS registrieren, indem sie AvSetMmThreadCharacteristics mit einem Tasknamen aufrufen, der einem der Unterschlüssel von HKLM\Software\­ Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile\Tasks entspricht. (Die Liste kann von OEMs bearbeitet werden, um nach Bedarf weitere Tasks hinzuzufügen.) Im Lieferzustand sind die folgenden Tasks vorhanden: QQ Audio QQ Capture QQ Distribution QQ Games QQ Low Latency QQ Playback QQ Pro Audio QQ Window Manager 282 Kapitel 4 Threads
Jeder dieser Tasks schließt Informationen über die Eigenschaften ein, durch die sie sich unterscheiden. Die wichtigste Eigenschaft für die Threadplanung ist die Zeitplanungskategorie, die einer der wichtigsten Faktoren dafür ist, die Priorität der bei MMCSS registrierten Threads zu bestimmen. Tabelle 4–4 zeigt die verschiedenen Planungskategorien. Kategorie Priorität Beschreibung Hoch 23–26 Pro-Audio-Threads, die mit einer höheren Priorität als alle anderen Threads auf dem System laufen (ausgenommen kritische Systemthreads) Mittel 16–22 Threads einer Vordergrundanwendung wie Windows Media Player Niedrig 8–15 Alle anderen Threads, die nicht zu einer der vorherigen Kategorien gehören Erschöpft 4–6 Threads, die ihren Anspruch auf die CPU erschöpft haben und nur dann weiter ausgeführt werden, wenn keine Threads höherer Priorität auf der CPU laufen Tabelle 4–4 Zeitplanungskategorien Der Hauptmechanismus, der hinter MMCSS steht, erhöht die Priorität der Threads in einem registrierten Prozess für einen garantierten Zeitraum auf die Stufe, die ihrer Zeitplanungskategorie und ihrer relativen Priorität in dieser Kategorie entspricht. Danach versetzt er die Threads in die Kategorie Erschöpft, sodass andere, Nicht-Multimediathreads auf dem System eine Gelegenheit zur Ausführung bekommen. Standardmäßig erhalten Multimediathreads 80 % der verfügbaren CPU-Zeit, während andere Threads 20 % erhalten. (Bei einer Stichprobe von 10 ms wären das 8 bzw. 2 ms.) Diese Verteilung können Sie in dem Registrierungswert SystemResponsiveness unter HKLM\SOFTWARE\ Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile ändern. Der Wert lässt sich im Bereich von 10 bis 100 % einstellen (wobei 20 der Standardwert ist). Bei Angabe eines niedrigeren Wertes als 10 wird 10 festgelegt. Der Wert gibt den prozentualen Anteil an der CPU an, der dem System (also nicht den registrierten Audio-Apps) garantiert ist. Der MMCSS-Planungsthread läuft auf einer Priorität von 27, da er jeden Pro-Audio-Thread vorzeitig unterbrechen muss, um dessen Priorität auf die Kategorie Erschöpft abzusenken. Wie bereits erwähnt, ist es gewöhnlich nicht sinnvoll, die relativen Prioritäten der Threads innerhalb eines Prozesses zu ändern. Es gibt auch kein Werkzeug, mit dem das möglich wäre, da nur die Entwickler die Wichtigkeit der einzelnen Threads in ihren Programmen genau kennen. Da sich Anwendungen manuell bei MMCSS registrieren und Informationen über die Art ihres Threads angeben müssen, hat MMCSS dagegen die erforderlichen Daten, um die relativen Threadprioritäten zu ändern. Die Entwickler sind sich auch im Klaren darüber, dass dies geschehen kann. Threadplanung Kapitel 4 283
Experiment: Prioritätserhöhung durch MMCSS In diesem Experiment sehen Sie sich die Auswirkungen der MMCSS-Prioritätserhöhung an. 1. Führen Sie den Windows Media Player (Wmplayer.exe) aus. (Andere Abspielprogramme nutzen möglicherweise nicht die API-Aufrufe, die zur Registrierung bei MMCSS erforderlich sind.) 2. Spielen Sie einige Audioinhalte ab. 3. Richten Sie mit dem Task-Manager oder mit Process Explorer die Affinität des Wmplayer­. exe-Prozesses so ein, dass er nur auf einer CPU ausgeführt wird. 4. Starten Sie die Leistungsüberwachung. 5. Ändern Sie die Prioritätsklasse der Leistungsüberwachung im Task-Manager auf Echtzeit, sodass sie die Aktivitäten besser aufzeichnen kann. 6. Klicken Sie in der Symbolleiste auf Leistungsindikator hinzufügen oder drücken Sie (Strg) + (I), um das Dialogfeld Leistungsindikatoren hinzufügen zu öffnen. 7. Wählen Sie das Objekt Thread und darunter den Indikator Aktuelle Priorität aus. 8. Geben Sie Wmplayer in das Feld Instanzen ein, klicken Sie auf Suchen und markieren Sie alle Threads dieses Prozesses. 9. Klicken Sie auf Hinzufügen und dann auf OK. 10. Klicken Sie auf das Aktionsmenü und wählen Sie Eigenschaften. 11. Ändern Sie auf der Registerkarte Grafik das Maximum der vertikalen Skalierung auf 32. Es sollten ein oder mehrere Priorität-16-Threads innerhalb von Wmplayer angezeigt werden, die ständig ausgeführt werden, sofern kein Thread höherer Priorität die CPU beansprucht, nachdem er in die Kategorie Erschöpft gefallen ist. 12. Starten Sie CPU Stress. 13. Setzen Sie den Aktivitätsgrad von Thread 1 auf Maximum. 14. Die Priorität von Thread 1 beträgt Normal. Ändern Sie sie in Time Critical. 15. Ändern Sie die Prioritätsklasse von CPUSTRES in High. 16. Ändern Sie die Affinität von CPUSTRES auf dieselbe CPU wie für Wmplayer. Das System sollte jetzt erheblich langsamer werden, doch die Musikwiedergabe sollte fortfahren. Das System wird ab und zu wieder reagieren. → 284 Kapitel 4 Threads
17. In der Leistungsüberwachung können Sie beobachten, wie die Priorität-16-Threads von Wmplayer von Zeit zu Zeit abfallen: Die Funktion von MMCSS beschränkt sich jedoch nicht auf diese einfache Prioritätserhöhung. Aufgrund der Natur der Netzwerktreiber in Windows und auf dem NDIS-Stack sind DPCs gängige Mechanismen dafür, Aufgaben zu verzögern, bis ein Interrupt von einer Netzwerkkarte eingegangen ist. Da DPCs auf einer höheren IRQL ausgeführt werden als Benutzermoduscode (siehe Kapitel 6), kann lang laufender Treibercode für Netzwerkkarten die Medienwiedergabe nach wie vor unterbrechen, etwa bei Netzwerkübertragungen oder beim Spielen eines Spiels. MMCSS sendet einen besonderen Befehl an den Netzwerkstack, um ihn anzuweisen, die Netzwerkpakete während der Dauer der Mediapakete zu drosseln. Das dient dazu, die Wiedergabeleistung zu maximieren, wofür kleine Einbußen beim Netzwerkdurchsatz in Kauf genommen werden (die bei Netzwerkoperationen, die während der Wiedergabe gewöhnlich durchgeführt werden, etwa beim Spielen eines Onlinespiels, nicht bemerkbar sind). Der Mechanismus dafür gehört jedoch nicht zum Scheduler, weshalb wir ihn hier nicht beschreiben. MMCSS unterstützt auch das sogenannte »Deadline Scheduling« (Threadplanung mit Termin). Dies fußt auf der Grundüberlegung, dass ein Programm zur Audiowiedergabe nicht immer die höchste Prioritätsstufe in seiner Kategorie benötigt. Wenn es die Audiodaten puffert und den Puffer abspielt, während es den nächsten Puffer auffüllt, kann der Clientthread angeben, zu welchem Zeitpunkt er die hohe Prioritätsstufe zur Vermeidung von Aussetzern benötigt, und in der Zwischenzeit mit einer leicht geringeren Priorität (innerhalb seiner Kategorie) leben. Mit der Funktion AvTaskIndexYield kann ein Thread angeben, wann er die höchste Threadplanung Kapitel 4 285
Priorität in seiner Kategorie erhalten muss, um das nächste Mal ausgeführt zu werden. Bis dieser Zeitpunkt gekommen ist, erhält er die geringste Priorität in seiner Kategorie, sodass mehr CPU-Zeit für das System zur Verfügung steht. Autoboost Das Autoboost-Framework soll das zuvor beschriebene Problem der Prioritätsumkehrung angehen. Dabei werden die Threads verfolgt, die Sperren besitzen, und diejenigen, die darauf warten, sodass es möglich ist, die Prioritäten der passenden Threads zu steigern (bei Bedarf auch die E/A-Prioritäten), um die Verarbeitung voranzubringen. Die Informationen über die Sperren werden in der KTHREAD-Struktur in einem statischen Array aus KLOCK_ENTRY-Objekten gespeichert. Bei der aktuellen Implementierung gibt es höchstens sechs solcher Einträge, von denen jeder zwei Binärbäume unterhält, einen für die Sperren, die der Thread besitzt, und einen für die Sperren, auf die der Thread wartet. Diese Bäume sind mit Schlüsseln nach ihrer Priorität versehen, weshalb zur Bestimmung der höchsten Priorität, auf die eine Erhöhung angewendet werden soll, stets eine konstante Zeit erforderlich ist. Wird eine Erhöhung benötigt, so wird die Priorität des Besitzerthreads auf die des wartenden Threads gesetzt. Auch die E/A-Priorität kann erhöht werden, wenn sie zu niedrig ist (siehe Kapitel 6). Wie bei allen Prioritätserhöhungen kann auch mit Autoboost maximal eine Priorität von 15 erreicht werden. (Die Priorität von Echtzeitthreads wird nicht erhöht.) In der aktuellen Implementierung wird Autoboost für Pushlocks und abgesicherte Mutexsynchronisierungsprimitiva verwendet, die nur für Kernelcode verfügbar gemacht werden (siehe Kapitel 8 in Band 2). Des Weiteren wird dieses Framework von einigen Komponenten der Exekutive für besondere Fälle genutzt. In zukünftigen Versionen von Windows kann Autoboost auch für Objekte implementiert werden, die vom Benutzermodus aus zugänglich sind und bei denen es das Prinzip des Besitzes gibt, z. B. für kritische Abschnitte. Kontextwechsel Der Kontext eines Threads und die Prozedur für den Kontextwechsel hängen von der Architektur des Prozessors ab. Gewöhnlich ist es bei einem Kontextwechsel erforderlich, folgende Daten zu speichern und neu zu laden: QQ Anweisungszeiger QQ Kernelstackzeiger QQ Zeiger auf den Adressraum, in dem der Thread ausgeführt wird (Seitentabellenverzeichnis des Prozesses) Der Kernel speichert diese Informationen des alten Threads, indem er sie auf den aktuellen Kernelmodusstack legt (den des alten Threads), den Stackzeiger aktualisiert und diesen dann in der KTHREAD-Struktur des alten Threads speichert. Anschließend wird der Kernelstackzeiger auf den Kernelstack des neuen Threads gesetzt und der Kontext dieses Threads geladen. Befindet sich der neue Thread in einem anderen Prozess, so wird die Adresse von dessen Seitentabellenverzeichnis in ein besonderes Prozessorregister geladen, damit der Adressraum zur Verfügung steht (siehe die Beschreibung der Adressübersetzung in Kapitel 5). Bei einem ausstehenden 286 Kapitel 4 Threads
Kernel-APC wird ein Interrupt auf IRQL 1 angefordert. (Mehr über APCs erfahren Sie in Kapitel 8 von Band 2.) Anderenfalls geht die Steuerung an den wiederhergestellten Anweisungszeiger des neuen Threads über, woraufhin der neue Thread mit der Ausführung beginnt. Direct Switch In Windows 8 und Windows Server 2012 wurde die Optimierung Direct Switch eingeführt, die es einem Thread ermöglicht, sein Quantum und seine Prioritätserhöhung einem anderen Thread zu spenden, der dann sofort auf demselben Prozessor ausgeführt wird. Bei synchroner Client/Server-Verarbeitung kann dieser direkte Wechsel zu einer erheblichen Verbesserung des Durchsatzes führen, da die Client- und Serverthreads nicht zu anderen Prozessoren verlegt werden, die sich im Leerlauf befinden oder geparkt sein können. Anderes ausgedrückt, kann immer nur ein Client- oder Serverthread ausgeführt werden, weshalb der Scheduler sie als einen einzigen logischen Thread behandeln sollte. Abb. 4–13 zeigt die Auswirkungen von Direct Switch. Ohne Direct Switch T1 geht in den Wartezustand über und weckt T2 auf Mit Direct Switch T1 geht in den Wartezustand über und weckt T2 auf Abbildung 4–13 Direct Switch Der Scheduler kann nicht wissen, dass der erste Thread (T1 in Abb. 4–13) in einen Wartezustand eintritt, nachdem er ein Synchronisierungsobjekt signalisiert hat, auf das der zweite Thread (T2) wartet. Daher muss eine besondere Funktion aufgerufen werden, um den Scheduler darüber zu informieren (atomares Signalisieren und Warten). Wenn möglich, führt die Funktion KiDirectSwitchThread den eigentlichen Kontextwechsel aus. Sie wird von KiExitDispatcher aufgerufen, wenn ein Flag übergeben wird, das die Möglichkeit eines direkten Wechsels anzeigt. Der erste Thread spendet seine Priorität dem zweiten (sofern dessen Priorität niedriger ist als die des ersten), wenn dies KiExitDispatcher in einem weiteren Bitflag angezeigt wird. In der aktuellen Implementierung werden immer entweder beide Flags oder keines angegeben, was bedeutet, dass bei jedem direkten Wechsel auch eine Prioritätsspende erfolgt. Der direkte Wechsel kann jedoch fehlschlagen, etwa wenn die Affinität des Zielthreads verhindert, dass er auf dem aktuellen Prozessor ausgeführt wird. Hat er jedoch Threadplanung Kapitel 4 287
Erfolg, so wird das Quantum des ersten Threads auf den zweiten übertragen, wobei der erste Thread sein restliches Quantum einbüßt. Direkte Kontextwechsel werden zurzeit in folgenden Situationen verwendet: QQ Wenn ein Thread die Windows-API SignalObjectAndWait (oder ihr Gegenstück NtSignal­ AndWaitForSingleObject im Kernel) aufruft QQ ALPCs (siehe Kapitel 8 in Band 2) QQ Synchrone Remoteprozeduraufrufe (RPCs) QQ COM-Remoteaufrufe (zurzeit nur von MTA zu MTA [Multithreadapartment]) Mögliche Fälle bei der Threadplanung Windows entscheidet aufgrund der Threadpriorität, welcher Thread als Nächstes die CPU bekommt. Wie aber funktioniert das in der Praxis? In diesem Abschnitt sehen wir uns an, wie das prioritätsgestützte präemptive Multitasking auf Threadebene abläuft. Freiwilliger Wechsel Ein Thread kann den Prozessor freiwillig aufgeben, wenn er auf ein Objekt wartet (z. B. ein Ereignis, einen Mutex, einen Semaphor, einen E/A-Abschlussport, einen Prozess, einen Thread usw.) und eine der Windows-Wartefunktionen wie WaitForSingleObject oder WaitForMultipleObjects aufruft (siehe Kapitel 8 in Band 2). Abb. 4–14 zeigt, wie ein Thread in einen Wartezustand eintritt und Windows einen neuen Thread zur Ausführung auswählt. Der oberste Kasten (Thread) in dieser Abbildung gibt den Prozessor freiwillig auf, sodass der nächste Thread in der Bereitschaftswarteschlange ausgeführt werden kann (angezeigt durch den »Heiligenschein« in der Spalte Wird ausgeführt). In dieser Abbildung mag es zwar so aussehen, als würde die Priorität des ersten Threads verringert, doch das ist nicht der Fall. Er wird einfach in die Warteschlange für die Objekte verlagert, auf die er wartet. Priorität Wird ausgeführt Bereit Zum Wartezustand Abbildung 4–14 Freiwilliger Kontextwechsel 288 Kapitel 4 Threads
Vorzeitige Unterbrechung In dieser Situation wird ein Thread unterbrochen, weil ein Thread mit höherer Priorität ausführungsbereit weit. Dies kann aus den beiden folgenden Gründen geschehen: QQ Der Thread höherer Priorität beendet seinen Wartezustand (weil das Ereignis, auf das er gewartet hat, eingetreten ist) QQ Die Priorität eines Threads wurde erhöht oder verringert. In jedem dieser beiden Fälle muss Windows herausfinden, ob der zurzeit laufende Thread weiterhin ausgeführt oder vorzeitig unterbrochen werden soll, um einem Thread höherer Priorität das Feld zu überlassen. Threads im Benutzermodus können Threads im Kernelmodus vorzeitig unterbrechen. Der Modus des Threads spielt keine Rolle; es kommt nur auf die Priorität an. Wird ein Thread vorzeitig beendet, so wird er an den Anfang der Bereitschaftswarteschlange für seine Priorität gestellt (siehe Abb. 4–15). Priorität Wird ausgeführt Bereit Zum Wartezustand Abbildung 4–15 Präemptive Threadplanung In Abb. 4–15 kehrt ein Thread mit der Priorität 18 aus seinem Wartezustand zurück und übernimmt die CPU, weshalb der zuvor laufende Thread mit Priorität 16 an den Anfang der Bereitschaftswarteschlange gestellt wird – nicht ans Ende! Wenn der Thread höherer Priorität ausgeführt ist, kann der unterbrochene Thread dadurch weitermachen. Ablauf des Quantums Wenn der laufende Thread sein CPU-Quantum erschöpft hat, muss Windows ermitteln, ob seine Priorität verringert und ob ein anderer Thread auf dem Prozessor eingeplant werden soll. Wird die Threadpriorität gesenkt (z. B. weil der Thread zuvor einen Prioritätsschub erhalten hat), so sucht Windows nach einem geeigneteren Thread, z. B. einem Thread in einer Bereitschaftswarteschlange mit einer höheren Priorität als der neuen Priorität des laufenden Threads. Threadplanung Kapitel 4 289
Wird die Threadpriorität dagegen nicht gesenkt, so wählt Windows den nächsten Thread aus der Bereitschaftswarteschlange für die betreffende Priorität aus. Anschließend stellt es den bisherigen Thread ans Ende der Warteschlange, gibt ihm einen neuen Quantumswert und ändert seinen Status von Wird ausgeführt in Bereit (siehe Abb. 4–16). Steht kein Thread derselben Priorität bereit, dann darf der Thread noch ein weiteres Quantum lang laufen. Priorität Wird ausgeführt Bereit Abbildung 4–16 Threadplanung nach Ablauf eines Quantums Wie Sie bereits gesehen haben, verlässt sich Windows zur Planung der Threads nicht auf ein Quantum auf der Grundlage des Taktintervalltimers, sondern bestimmt die Quantumssollwerte anhand des genauen CPU-Taktzykluszählers. Diesen Zähler nutzt es auch, um zu bestimmen, ob es überhaupt angemessen ist, das Quantum eines Threads für beendet zu erklären. Eine unangemessene Beendigung konnte früher vorkommen. Dies ist ein wichtiges Thema, das wir erörtern müssen. Bei der Planung auf der Grundlage des Taktintervalltimers kann folgende Situation auftreten: 1. Die Threads A und B werden in der Mitte eines Intervalls ausführungsbereit. Da der Planungscode nicht nur bei jedem Taktintervall ausgeführt wird, tritt diese Situation häufig auf. 2. Thread A beginnt mit der Ausführung, wird aber eine Zeit lang unterbrochen. Die Zeit für die Handhabung des Interrupts wird dem Thread zugeschlagen. 3. Die Interruptverarbeitung endet, sodass Thread A wieder zu laufen beginnt. Dabei erreicht er jedoch schnell das nächste Taktintervall. Der Scheduler muss annehmen, dass Thread A die ganze Zeit ausgeführt worden ist, und schaltet zu Thread B um. 4. Thread B beginnt mit der Ausführung und darf ein ganzes Taktintervall lang laufen (sofern es nicht zu vorzeitiger Unterbrechung oder zu einem Interrupt kommt). In diesem Beispiel wurde Thread A gleich zweifach ungerecht bestraft. Erstens wurde die Zeit für die Handhabung eines Geräteinterrupts auf seine CPU-Zeit angerechnet, auch wenn der Thread mit diesem Interrupt wahrscheinlich gar nichts zu tun hatte. (Interrupts werden immer im Kontext des aktuellen Threads gehandhabt, wie Sie in Kapitel 6 noch sehen werden.) Außerdem wurde er für die Zeit bestraft, die das System vor seiner Einplanung in demselben Intervall im Leerlauf zugebracht hat. Abb. 4–17 stellt diese Situation dar. 290 Kapitel 4 Threads
Threads A und B werden bereit zur Ausführung Thread A Leerlauf Thread B Interrupt Intervall 1 Intervall 2 Abbildung 4–17 Unfaire Zeitaufteilung in Windows-Versionen vor Vista Moderne Windows-Versionen führen einen genauen Zähler der CPU-Taktzyklen, die der Thread für die Arbeiten verwendet, für die er eingeplant wurde (was bedeutet, dass die Verarbeitung von Interrupts dabei nicht mitgezählt wird). Sie merken sich auch einen Sollwert von Taktzyklen, die der Thread am Ende seines Quantums verbraucht haben soll. Daher können die beiden unfairen Entscheidungen zulasten von Threads in dem vorherigen Abschnitt nicht mehr auftreten. Stattdessen geschieht Folgendes: 1. Die Threads A und B werden in der Mitte eines Intervalls ausführungsbereit. 2. Thread A beginnt mit der Ausführung, wird aber eine Zeit lang unterbrochen. Die CPU-Taktzyklen für die Handhabung des Interrupts werden dem Thread nicht angerechnet. 3. Die Interruptverarbeitung endet, sodass Thread A wieder zu laufen beginnt. Dabei erreicht er jedoch schnell das nächste Taktintervall. Der Scheduler vergleicht die dem Thread angerechneten CPU-Taktzyklen mit dem Sollwert für das Ende des Quantums. 4. Da die Anzahl der Taktzyklen viel kleiner ist, als sie sein sollte, geht der Scheduler davon aus, dass Thread A in der Mittel eines Taktintervalls mit der Ausführung begonnen und möglicherweise auch noch unterbrochen wurde. 5. Das Quantum von Thread A wird um ein weiteres Taktintervall verlängert. Der Quantumssollwert wird neu berechnet. Jetzt hat Thread A die Chance, für ein komplettes Taktintervall zu laufen. 6. Am Ende des nächsten Taktintervalls hat Thread A sein Quantum erschöpft, weshalb Thread B an die Reihe kommt. Diesen Ablauf veranschaulicht Abb. 4–18. Threads A und B werden bereit zur Ausführung Leerlauf Thread A Thread B Intervall 2 Intervall 3 Interrupt Intervall 1 Abbildung 4–18 Unfaire Zeitaufteilung in Windows-Versionen vor Vista Threadplanung Kapitel 4 291
Terminierung Wenn die Ausführung eines Threads endet (weil er von seiner Hauptroutine zurückgesprungen ist, ExitThread ausgerufen hat oder durch TerminateThread beendet wurde), wechselt er vom Status Wird ausgeführt zu Abgebrochen. Sind keine Handles für das Threadobjekt mehr geöffnet, wird der Thread aus der Threadliste des Prozesses entfernt. Die Zuweisung der zugehörigen Datenstrukturen wird aufgehoben. Leerlaufthreads Wenn es auf einer CPU keinen ausführbaren Thread gibt, disponiert Windows den Leerlaufthread der CPU. Jede CPU hat ihren eigenen Leerlaufthread, da es auf einem Mehrprozessorsystem passieren kann, dass eine CPU einen Thread ausführt, während auf den anderen keine Threads zur Verfügung stehen. Der Leerlaufthread einer CPU wird über einen Zeiger in ihrem PRCB gefunden. Alle Leerlaufthreads gehören zum Leerlaufprozess. Sowohl die Leerlaufthreads als auch der Leerlaufprozess sind Sonderfälle, und das in mehrerer Hinsicht. Sie werden zwar durch EPROCESS- und KPROCESS- bzw. ETHREAD- und KTHREAD-Strukturen dargestellt, aber es handelt sich bei ihnen nicht um Exekutivprozesse und Threadobjekte. Der Leerlaufprozess steht auch nicht in der Prozessliste des Systems. (Das ist auch der Grund, warum er in der Ausgabe des Kerneldebuggerbefehls !process 00 nicht erscheint.) Allerdings lassen sich Leerlaufthreads und ihr Prozess auf andere Weise finden. Experiment: Die Strukturen der Leerlaufthreads und des Leerlaufprozesses anzeigen Die Strukturen der Leerlaufthreads und des Leerlaufprozesses können Sie im Kerneldebugger mit dem Befehl !pcr einsehen (Process Control Region). Er zeigt einen Teil der Informationen der PCR und des zugehörigen PRCBs an. !pcr nimmt ein einziges numerisches Argument entgegen, nämlich die Nummer der CPU, deren PCR angezeigt werden soll. Der Bootprozessor ist dabei Prozessor 0. Da er stets vorhanden ist, sollte !pcr 0 immer funk­ tionieren. Die folgende Ausgabe zeigt das Ergebnis dieses Befehls auf einem 64-Bit-System mit acht Prozessoren: lkd> !pcr KPCR for Processor 0 at fffff80174bd0000: Major 1 Minor 1 NtTib.ExceptionList: fffff80176b4a000 NtTib.StackBase: fffff80176b4b070 NtTib.StackLimit: 000000000108e3f8 NtTib.SubSystemTib: fffff80174bd0000 NtTib.Version: 0000000074bd0180 NtTib.UserPointer: fffff80174bd07f0 NtTib.SelfTib: 00000098af072000 → 292 Kapitel 4 Threads
SelfPcr: 0000000000000000 Prcb: fffff80174bd0180 Irql: 0000000000000000 IRR: 0000000000000000 IDR: 0000000000000000 InterruptMode: 0000000000000000 IDT: 0000000000000000 GDT: 0000000000000000 TSS: 0000000000000000 CurrentThread: ffffb882fa27c080 NextThread: 0000000000000000 IdleThread: fffff80174c4c940 DpcQueue: Zu dem Zeitpunkt, an dem dieser Speicherauszug aufgenommen wurde, hat CPU 0 einen anderen als den Leerlaufthread ausgeführt, was Sie daran erkennen können, dass sich die Zeiger CurrentThread und IdleThread unterscheiden. Auf einem Mehrprozessorsystem können Sie auch !pcr 1, !pcr 2 usw. ausprobieren, bis Sie alle Prozessoren durchlaufen haben. Dabei werden Sie feststellen, dass sich die einzelnen IdleThread-Zeiger unterscheiden. Wenden Sie nun den Befehl !thread auf die hervorgehobene Adresse des Leerlaufthreads an: lkd> !thread fffff80174c4c940 THREAD fffff80174c4c940 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0 Not impersonating DeviceMap ffff800a52e17ce0 Owning Process fffff80174c4b940 Image: Idle Attached Process ffffb882e7ec7640 Image: System Wait Start TickCount 1637993 Ticks: 30 (0:00:00:00.468) Context Switch Count 25908837 IdealProcessor: 0 UserTime 00:00:00.000 KernelTime 05:51:23.796 Win32 Start Address nt!KiIdleLoop (0xfffff801749e0770) Stack Init fffff80176b52c90 Current fffff80176b52c20 Base fffff80176b53000 Limit fffff80176b4d000 Call 0000000000000000 Priority 0 BasePriority 0 PriorityDecrement 0 IoPriority 0 PagePriority 5 Als Letztes wenden Sie den Befehl !process auf den Besitzerprozess an, der in der vorstehenden Ausgabe gezeigt wird. Der Kürze halber haben wir im folgenden Beispiel einen zweiten Parameter mit dem Wert 3 angegeben, damit !process nur ein Minimum an Informationen über jeden Thread anzeigt: lkd> !process fffff80174c4b940 3 PROCESS fffff80174c4b940 → Threadplanung Kapitel 4 293
SessionId: none Cid: 0000 Peb: 00000000 ParentCid: 0000 DirBase: 001aa000 ObjectTable: ffff800a52e14040 HandleCount: 2011. Image: Idle VadRoot ffffb882e7e1ae70 Vads 1 Clone 0 Private 7. Modified 1627. Locked 0. DeviceMap 0000000000000000 Token ffff800a52e17040 ElapsedTime 07:07:04.015 UserTime 00:00:00.000 KernelTime 00:00:00.000 QuotaPoolUsage[PagedPool] 0 QuotaPoolUsage[NonPagedPool] 0 Working Set Sizes (now,min,max) (7, 50, 450) (28KB, 200KB, 1800KB) PeakWorkingSetSize 1 VirtualSize 0 Mb PeakVirtualSize 0 Mb PageFaultCount 2 MemoryPriority BACKGROUND BasePriority 0 CommitCharge 0 THREAD fffff80174c4c940 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0 THREAD ffff9d81e230ccc0 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 1 THREAD ffff9d81e1bd9cc0 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 2 THREAD ffff9d81e2062cc0 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 3 THREAD ffff9d81e21a7cc0 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 4 THREAD ffff9d81e22ebcc0 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 5 THREAD ffff9d81e2428cc0 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 6 THREAD ffff9d81e256bcc0 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 7 Diese Prozess- und Threadadressen können Sie auch für dt nt!_EPROCESS, dt nt!_KTHREAD und ähnliche Befehle verwenden. Das vorstehende Experiment zeigt einige der Anomalien im Zusammenhang mit dem Leerlaufprozess und seinen Threads. Im Debugger wird der Abbildname Idle angezeigt (der 294 Kapitel 4 Threads
aus dem Element ImageFileName der EPROCESS-Struktur entnommen ist), doch verschiedene Windows-Hilfsprogramme geben andere Namen an – etwa System Idle Process (oder Leerlaufprozess) im Task-Manager und in Process Explorer oder System Process in tlist. Die Prozess-ID und die Thread-IDs (die Client-IDs oder Cid-Werte in der Ausgabe des Debuggers) sind 0, ebenso die PEB- und TEB-Zeiger und möglicherweise auch noch viele andere Felder des Leerlaufprozesses und seiner Threads. Da der Leerlaufprozess keinen Benutzermodus-Adressraum hat und seine Threads keinen Benutzermoduscode ausführen, werden auch die sonst erforderlichen Daten zur Verwaltung einer Benutzermodusumgebung nicht gebraucht. Der Leerlaufprozess ist auch kein Prozessobjekt des Objekt-Managers, und die Leerlaufthreads sind keine Threadobjekte. Stattdessen werden die Strukturen für den ersten Leerlaufthread und den Leerlaufprozess statisch zugewiesen und zum Hochfahren des Systems verwendet, bevor der Prozess- und der Objekt-Manager initialisiert sind. Die Strukturen der nachfolgenden Leerlaufthreads werden dynamisch zugewiesen (als einfache Zuweisungen vom nicht ausgelagerten Pool, wobei der Objekt-Manager umgangen wird), wenn weitere Prozessoren online kommen. Nachdem die Prozessverwaltung initialisiert ist, verwendet sie die besondere Variable PsIdleProcess, um auf den Leerlaufprozess zu verweisen. Die vielleicht bemerkenswerteste Anomalie besteht darin, dass Windows als Priorität des Leerlaufprozesses 0 angibt. In der Praxis sind die Prioritätswerte der Leerlaufthreads jedoch ohne Bedeutung, da diese Threads nur dann ausgewählt werden, wenn keine anderen Threads zur Ausführung anstehen. Ihre Priorität wird niemals mit der von anderen Threads verglichen oder dazu herangezogen, einen Leerlaufthread in eine Bereitschaftswarteschlange zu stellen. (Auf einem Windows-System läuft tatsächlich immer nur ein Thread mit der Priorität 0, nämlich der Nullseitenthread, der in Kapitel 5 ausführlicher erläutert wird.) Leerlaufthreads bilden nicht nur einen Sonderfall bei der Auswahl zur Ausführung, sondern auch bei der vorzeitigen Unterbrechung. Die Routine KiIdleLoop eines Leerlaufthreads führt eine Reihe von Operationen durch, die verhindern, dass er von einem anderen Thread auf die übliche Weise vorzeitig unterbrochen wird. Stehen Nicht-Leerlaufthreads zur Ausführung auf einem Prozessor bereit, wird der Prozessor in seinem PRCB als unbeschäftigt gekennzeichnet. Wird ein Thread zur Ausführung auf diesem Prozessor ausgewählt, so wird seine Adresse im Zeiger NextThread des PRCBs für diesen Prozessor gespeichert. Bei jedem Schleifendurchlauf überprüft der Leerlaufthread diesen Zeiger. Der grundlegende Ablauf der Operationen eines Leerlaufthreads sieht wie folgt aus, wobei einige Einzelheiten jedoch von der jeweiligen Architektur abhängen (dies ist eine der wenigen Routinen, die in Assembler statt in C geschrieben sind): 1. Der Leerlaufthread lässt kurzzeitig Interrupts zu, damit ausstehende Interrupts ausgeliefert werden können, und deaktiviert sie dann wieder (mit den Anweisungen STI und CLI auf x86- und x64-Prozessoren). Das ist sinnvoll, da große Teile des Leerlaufthreads mit deaktivierten Interrupts ausgeführt werden. 2. Im Testbuild auf einigen Architekturen prüft der Leerlaufthread, ob ein Kerneldebugger versucht, einen Haltepunkt in dem System auszulösen. Wenn ja, gibt er ihm Zugriff. 3. Der Leerlaufthread prüft, ob es auf dem Prozessor ausstehende DPCs gibt (siehe Kapitel 6). Das kann der Fall sein, wenn die DPCs in eine Warteschlange gestellt, dabei aber keine DPC-Interrupts generiert wurden. Sind ausstehende DPCs vorhanden, ruft die Leerlauf- Threadplanung Kapitel 4 295
schleife KiRetireDpcList auf, um sie auszuliefern. Das führt auch zu einem Ablauf der Timer und zur Verarbeitung der Liste von Threads mit dem Status Verzögert bereit (siehe den Abschnitt »Mehrprozessorsysteme« weiter hinten). Der Start von KiRetireDpcList muss mit deaktivierten Interrupts erfolgen, weshalb dieser Zustand am Ende von Schritt 1 hergestellt wird. Auch bei der Beendigung von KiRetireDpcList sind Interrupts deaktiviert. 4. Der Leerlaufthread prüft, ob die Verarbeitung des Quantumsendes angefordert worden ist. Wenn ja, wird KiQuantumEnd aufgerufen, um diesen Vorgang durchzuführen. 5. Der Leerlaufthread prüft, ob ein Thread zur Ausführung auf dem Prozessor ausgewählt worden ist. Wenn ja, disponiert er den Thread. Das kann beispielsweise der Fall sein, wenn ein in Schritt 3 verarbeiteter DPC oder Timerablauf den Wartevorgang eines Threads beendet hat oder wenn ein anderer Prozessor einen Thread für diesen Prozessor ausgewählt hat, während die Leerlaufschleife ausgeführt wurde. 6. Falls verlangt, prüft der Leerlaufthread, ob Threads auf anderen Prozessoren ausführungsbereit sind, und plant wenn möglich einen davon lokal ein. Dieser Vorgang wird im Abschnitt »Der Leerlaufscheduler« weiter hinten erklärt. 7. Der Leerlaufthread ruft die registrierte Energieverwaltungsroutine des Prozessors auf (falls Energieverwaltungsfunktionen ausgeführt werden müssen). Dabei handelt es sich entweder um den Prozessorenergietreiber (wie Intelppm.sys) oder, falls kein solcher Treiber zur Verfügung steht, die HAL. Anhalten von Threads Mit den API-Funktionen SuspendThread und ResumeThread können Threads ausdrücklich angehalten und wieder aufgenommen werden. Jeder Thread verfügt über einen Anhaltezähler, der durch Anhalten erhalten bleibt und durch Wiederaufnehmen verringert wird. Beträgt der Zähler 0, kann der Thread ausgeführt werden, anderenfalls nicht. Beim Anhalten wird dem Thread ein Kernel-APC vorangestellt. Wechselt der Thread zur Ausführung, so wird zunächst dieser APC ausgeführt, der den Thread veranlasst, auf ein Ereignis zu warten, das bei der Wiederaufnahme des Threads signalisiert wird. Dieser Anhaltemechanismus hat jedoch einen merklichen Nachteil, wenn sich der Thread schon in einem Wartezustand befindet, während die Anforderung zum Anhalten hereinkommt, denn dadurch muss der Thread erst aufwachen, damit er angehalten werden kann. Dabei kann obendrein der Kernelstack des Threads wieder eingewechselt werden, falls er ausgelagert war. In Windows 8.1 und Windows Server 2012 R2 wurde ein schlanker Anhaltemechanismus (Lightweight Suspend) eingeführt, damit ein Thread im Wartezustand nicht durch den APC-Mechanismus angehalten wird, sondern dadurch, dass das Threadobjekt im Arbeitsspeicher direkt bearbeitet und als »angehalten« gekennzeichnet wird. Einfrieren und Tiefgefrieren Beim Einfrieren werden Prozesse in einen angehaltenen Zustand versetzt, aus dem sie durch den Aufruf von ResumeThread für ihre Threads nicht hervorgeholt werden können. Das ist sinnvoll, wenn das System eine UWP-App anhalten muss. Dies geschieht, wenn eine Windows-­ 296 Kapitel 4 Threads
Anwendung in den Hintergrund verlagert wird. Im Tablet-Modus kann das passieren, wenn eine andere Anwendung in den Vordergrund geholt wird, und im Desktop-Modus, wenn die Anwendung minimiert wird. In einem solchen Fall räumt das System der Anwendung etwa fünf Sekunden ein, um noch gewisse Arbeiten zu verrichten, gewöhnlich um den Anwendungsstatus zu speichern. Das ist wichtig, da Windows-Apps ohne Vorwarnung beendet werden können, wenn die Speicherressourcen zur Neige gehen. Beim nächsten Start der Anwendung kann dieser Status dann wieder geladen werden, sodass es für den Benutzer aussieht, als wäre sie gar nicht weg gewesen. Beim Einfrieren eines Prozesses werden alle Threads so angehalten, dass sie mit ResumeThread nicht wieder aufgeweckt werden können. Ein Flag in der KTHREAD-Struktur gibt an, ob der Thread eingefroren ist. Damit ein Thread ausgeführt werden kann, muss sein Anhaltezähler 0 betragen und das Flag frozen darf nicht gesetzt sein. Beim Tiefgefrieren können auch neu erstellte Threads in dem Prozess nicht gestartet werden. Wenn Sie beispielsweise CreateRemoteThreadEx aufrufen, um einen neuen Thread in einem tiefgefrorenen Prozess anzulegen, so wird dieser Thread schon eingefroren, bevor er startet. Das ist die übliche Vorgehensweise beim Einfrieren. Die Möglichkeiten zum Einfrieren von Prozessen und Threads werden im Benutzermodus nicht direkt bereitgestellt, sondern intern vom PSM-Dienst genutzt (Process State Manager), der dafür zuständig ist, die Anforderungen zum Tiefgefrieren und Auftauen an den Kernel zu senden. Prozesse können Sie auch über die Jobs einfrieren. Die Möglichkeit, einen Job einzufrieren und aufzutauen, ist nicht öffentlich dokumentiert, steht aber mit dem Standardsystemaufruf NtSetInformationJobObject zur Verfügung. Er wird gewöhnlich für Windows-Anwendungen genutzt, da alle Prozesse solcher Anwendungen in Jobs enthalten sind. Ein Job kann einen einzigen Prozess enthalten (die Windows-Anwendung selbst), aber auch Hintergrundprozesse für die Anwendung. Mit dem genannten Aufruf können alle Prozesse in dem Job auf einmal eingefroren oder aufgetaut werden. (Mehr über Windows-Anwendungen erfahren Sie in Kapitel 8 von Band 2.) Experiment: Tiefgefrieren In diesem Experiment debuggen Sie eine virtuelle Maschine und beobachten dabei einen Tiefgefriervorgang. 1. Öffnen Sie WinDbg mit Administratorrechten und verbinden Sie den Debugger mit einer virtuellen Maschine mit Windows 10. 2. Drücken Sie (Strg) + (Untbr), um sich in die Ausführung der VM einzuklinken. 3. Setzen Sie einen Haltepunkt an der Stelle, an der das Tiefgefrieren beginnt. Verwenden Sie dazu einen Befehl, der zeigt, dass der Prozess eingefroren ist: bp nt!PsFreezeProcess "!process -1 0; g" 4. Geben Sie den Befehl g ein (go) oder drücken Sie (F5). Sie sollten jetzt viele Vorkommen von Tiefgefriervorgängen sehen. → Threadplanung Kapitel 4 297
5. Starten Sie die Cortana-Benutzeroberfläche über die Taskleiste und schließen Sie sie. Nach ungefähr fünf Sekunden sollten Sie folgende Ausgabe sehen: PROCESS 8f518500 SessionId: 2 Cid: 12c8 Peb: 03945000 ParentCid: 02ac DirBase: 054007e0 ObjectTable: b0a8a040 HandleCount: 988. Image: SearchUI.exe 6. Wechseln Sie wieder in den Debugger und lassen Sie sich weitere Informationen über den Prozess anzeigen: 1: kd> !process 8f518500 1 PROCESS 8f518500 SessionId: 2 Cid: 12c8 Peb: 03945000 ParentCid: 02ac DeepFreeze DirBase: 054007e0 ObjectTable: b0a8a040 HandleCount: 988. Image: SearchUI.exe VadRoot 95c1ffd8 Vads 405 Clone 0 Private 7682. Modified 201241. Locked 0. DeviceMap a12509c0 Token b0a65bd0 ElapsedTime 04:02:33.518 UserTime 00:00:06.937 KernelTime 00:00:00.703 QuotaPoolUsage[PagedPool] 562688 QuotaPoolUsage[NonPagedPool] 34392 Working Set Sizes (now,min,max) (20470, 50, 345) (81880KB, 200KB, 1380KB) 7. PeakWorkingSetSize 25878 VirtualSize 367 Mb PeakVirtualSize 400 Mb PageFaultCount 307764 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 8908 Job 8f575030 Beachten Sie das Attribut DeepFreeze, das der Debugger schreibt. Da der Prozess Teil eines Jobs ist, können Sie sich mit dem Befehl !job weitere Einzelheiten anzeigen lassen: 1: kd> !job 8f575030 Job at 8f575030 Basic Accounting Information TotalUserTime: 0x0 TotalKernelTime: 0x0 TotalCycleTime: 0x0 ThisPeriodTotalUserTime: 0x0 → 298 Kapitel 4 Threads
ThisPeriodTotalKernelTime: 0x0 TotalPageFaultCount: 0x0 TotalProcesses: 0x1 ActiveProcesses: 0x1 FreezeCount: 1 BackgroundCount: 0 TotalTerminatedProcesses: 0x0 PeakJobMemoryUsed: 0x38e2 PeakProcessMemoryUsed: 0x38e2 Job Flags [cpu rate control] [frozen] [wake notification allocated] [wake notification enabled] [timers virtualized] [job swapped] Limit Information (LimitFlags: 0x0) Limit Information (EffectiveLimitFlags: 0x3000) CPU Rate Control Rate = 100.00% Scheduling Group: a469f330 8. Der Job unterliegt der CPU-Ratensteuerung (siehe den Abschnitt »Grenzwerte für die CPU-Rate« weiter hinten in diesem Kapitel) und ist eingefroren. Lösen Sie die Verbindung zur VM und schließen Sie den Debugger. Threadauswahl Wenn ein logischer Prozessor den nächsten auszuführenden Thread auswählen muss, ruft er die Schedulerfunktion KiSelectNextThread auf. Das kann in verschiedenen Situationen geschehen: QQ Ein harter Affinitätswechsel ist aufgetreten, sodass der zurzeit laufende Thread oder der Standbythread nicht mehr auf dem betreffenden logischen Prozessor ausgeführt werden kann. Aus diesem Grund muss ein anderer ausgewählt werden. QQ Der zurzeit laufende Thread ist am Ende seines Quantums angekommen, und der SMTSatz, auf dem er ausgeführt wurde, ist beschäftigt, während sich andere SMT-Sätze innerhalb des idealen Knotens im Leerlauf befinden. (SMT oder symmetrisches Multithreading ist die technische Bezeichnung für die in Kapitel 2 beschriebene Hyperthreading-Technologie.) Der Scheduler verlagert den Thread, da sein Quantum erschöpft ist, weshalb ein neuer ausgewählt werden muss. QQ Ein Wartevorgang ist beendet, und im Wartestatusregister befinden sich ausstehende Planungsoperationen (anders ausgedrückt, die Prioritäts- und/oder Affinitätsbits sind gesetzt). Threadplanung Kapitel 4 299
In diesen Situationen geht der Scheduler wie folgt vor: QQ Der Scheduler ruft KiSelectReadyThreadEx auf, um nach dem nächsten ausführungsbereiten Thread zu suchen, der auf dem Prozessor ausgeführt werden kann, und um zu prüfen, ob einer gefunden wurde. QQ Wurde kein ausführungsbereiter Thread gefunden, wird der Leerlaufscheduler eingeschaltet und der Leerlaufthread zur Ausführung ausgewählt. Anderenfalls wird der ausführungsbereite Thread in die lokale oder die gemeinsam genutzte Bereitschaftswarteschlange gestellt. Die Operation KiSelectNextThread wird nur dann ausgeführt, wenn der logische Prozessor den nächsten planbaren Thread auswählen, aber noch nicht ausführen muss (weshalb der Thread in die Bereitschaftswarteschlange gestellt wird). Es kann jedoch auch sein, dass der logische Prozessor den nächsten Thread sofort ausführen oder, falls kein solcher verfügbar ist, eine andere Aktion erledigen möchte, anstatt in den Leerlauf überzugehen. Das kann unter folgenden Umständen geschehen: QQ Aufgrund eines Prioritätswechsels ist der Standbythread oder der laufende Thread nicht mehr der ausführungsbereite Thread mit höchster Priorität auf dem logischen Prozessor. Das bedeutet, dass jetzt ein ausführungsbereiter Thread mit höherer Priorität ausgeführt werden muss. QQ Der Thread hat den Prozessor mit YieldProcessor oder NtYieldExecution ausdrücklich aufgegeben und es steht möglicherweise ein anderer Thread zur Ausführung bereit. QQ Das Quantum des aktuellen Threads ist abgelaufen und andere Threads auf derselben Prioritätsstufe müssen die Gelegenheit zur Ausführung bekommen. QQ Ein Thread hat seine Prioritätserhöhung verloren, was zu einer ähnlichen Prioritätserhöhung führt wie im ersten Punkt. QQ Der Leerlaufscheduler läuft und prüft, ob zwischen der Anforderung und der Ausführung der Leerlaufplanung ein ausführungsbereiter Thread erschienen ist. Um herauszufinden, welche Routine jeweils eingesetzt wird, müssen Sie überlegen, ob der logische Prozessor einen anderen Thread ausführen muss (wobei KiSelectNextThread aufgerufen wird) oder ob ein anderer Thread nach Möglichkeit ausgeführt werden soll (in diesem Fall wird KiSelectReadyThreadEx verwendet). Da jeder Prozessor zu einer gemeinsamen Bereitschaftswarteschlange gehört (auf die der KPRCB zeigt), kann KiSelectReadyThreadEx in jedem Fall einfach die Warteschlangen des aktuellen logischen Prozessors prüfen und den ersten Thread höchster Priorität entnehmen, den sie findet, sofern seine Priorität geringer als diejenige des laufenden Threads ist (sofern der laufende Thread immer noch ausgeführt werden darf, was bei der Verwendung von KiSelectNextThread nicht der Fall ist). Gibt es keinen Thread höherer Priorität (oder überhaupt keine ausführungsbereiten Threads), wird kein Thread zurückgegeben. 300 Kapitel 4 Threads
Der Leerlaufscheduler Wenn der Leerlaufthread läuft, prüft er, ob die Leerlaufplanung aktiviert worden ist. Wenn ja, ruft er KiSearchForNewThread auf, um die Bereitschaftswarteschlangen der anderen Prozessoren nach Threads zu durchsuchen, die ausgeführt werden können. Die Laufzeitkosten für diese Operation werden nicht als Zeit für den Leerlaufthread angerechnet, sondern dem Prozessor als Interrupt- und DPC-Zeit. Die Leerlaufplanungszeit wird also als Systemzeit betrachtet. Der Algorithmus von KiSearchForNewThread basiert auf den zuvor in diesem Abschnitt beschriebenen Funktionen und wird in Kürze erklärt. Mehrprozessorsysteme Auf einem Einprozessorsystem ist die Threadplanung relativ einfach: Es wird stets derjenige der bereitstehenden Threads ausgeführt, der die höchste Priorität aufweist. Bei einem Mehrprozessorsystem ist die Lage jedoch etwas kniffliger, da Windows versucht, einen Thread auf dem am besten für ihn geeigneten Prozessor einzuplanen, wobei der bevorzugte und der vorherige Prozessor des Threads und die Konfiguration des Systems berücksichtigt werden. Windows versucht zwar nach wie vor, auf allen verfügbaren CPUs jeweils den lauffähigen Thread mit der höchsten Priorität auszuführen, kann aber nur garantieren, dass einer der Threads höchster Priorität irgendwo ausgeführt wird. Bei der Verwendung gemeinsamer Bereitschaftswarteschlangen (für Threads ohne Affinitätseinschränkungen) ist die Garantie stärker: Auf jeder Gruppe von Prozessoren wird mindestens einer der Threads höchster Priorität ausgeführt. Bevor wir den Algorithmus beschreiben, der auswählt, welche Threads wann und wo ausgeführt werden, schauen wir uns jedoch die zusätzlichen Informationen an, mit denen Windows auf den drei unterstützten Arten von Mehrprozessorsystemen (SMT, Mehrkernsysteme und NUMA) den Überblick über die Thread- und Prozessorstatus behält. Paket- und SMT-Sätze In Topologien mit logischen Prozessoren zieht Windows fünf Felder im KPRCB heran, um Threadplanungsentscheidungen zu treffen. Das erste Feld, CoresPerPhysicalProcessor, gibt an, ob der logische Prozessor Teil eines Mehrkernpakets ist. Es wird aus der vom Prozessor zurückgegebenen CPU-ID berechnet und auf eine Zweierpotenz gerundet. Das zweite Feld, LogicalProcessorsPerCore, gibt an, ob der logische Prozessor zu einem SMT-Satz gehört, wie es bei Intel-Prozessoren mit aktiviertem Hyperthreading der Fall ist. Es wird ebenfalls aufgrund der CPU-ID bestimmt und gerundet. Die Multiplikation dieser beiden Felder ergibt die Anzahl der logischen Prozessoren pro Paket, also pro physischem Prozessor auf einem Sockel. Damit füllt der PRCB sein Feld PackageProcessorSet aus. Dies ist die Affinitätsmaske, die angibt, welche anderen logischen Prozessoren in der Gruppe (Pakete sind auf eine Gruppe beschränkt) zum selben physischen Prozessor gehören. Auf ähnliche Weise werden logische Prozessoren über den Wert CoreProcessorSet mit demselben Kern verbunden, was als SMT-Satz bezeichnet wird. Der Wert GroupSetMember schließlich legt fest, welche Bitmaske in der aktuellen Prozessorgruppe für den vorliegenden logischen Prozessor steht. Beispielsweise hat der logische Prozessor 3 normalerweise den GroupSetMember-Wert 8 (23). Threadplanung Kapitel 4 301
Experiment: Informationen über logische Prozessoren einsehen Mit dem Kerneldebuggerbefehl !smt können Sie sich ansehen, welche Informationen Windows über SMT-Prozessoren führt. Die folgende Ausgabe stammt von einem Intel Core i7-Vierkernsystem mit SMT und acht logischen Prozessoren: lkd> !smt SMT Summary: -----------KeActiveProcessors: ********-------------------------------------------------------(00000000000000ff) IdleSummary: -****--*-------------------------------------------------------(000000000000009e) No PRCB SMT Set APIC Id 0 fffff803d7546180 **------------------------------- (0000000000000003) 0x00000000 1 ffffba01cb31a180 **------------------------------- (0000000000000003) 0x00000001 2 ffffba01cb3dd180 --**----------------------------- (000000000000000c) 0x00000002 3 ffffba01cb122180 --**----------------------------- (000000000000000c) 0x00000003 4 ffffba01cb266180 ----**--------------------------- (0000000000000030) 0x00000004 5 ffffba01cabd6180 ----**--------------------------- (0000000000000030) 0x00000005 6 ffffba01cb491180 ------**------------------------- (00000000000000c0) 0x00000006 7 ffffba01cb5d4180 ------**------------------------- (00000000000000c0) 0x00000007 Maximum cores per physical processor: 8 Maximum logical processors per core: 2 NUMA-Systeme Windows unterstützt auch Mehrprozessorsysteme mit nicht gleichförmiger Speicherarchitektur (Non-Uniform Memory Architecture, NUMA). Dabei sind die Prozessoren zu Einheiten gruppiert, die als Knoten bezeichnet werden. Jeder Knoten verfügt über seine eigenen Prozessoren und seinen eigenen Arbeitsspeicher und ist über einen cachekohärenten Bus mit dem umfas- 302 Kapitel 4 Threads
senderen System verbunden. Die Systeme werden als nicht gleichförmig bezeichnet, da jeder Knoten über seinen eigenen Hochgeschwindigkeitsspeicher verfügt. Jeder Prozessor kann zwar unabhängig von seinem Knoten auf den gesamten Arbeitsspeicher zugreifen, doch lokaler Knotenspeicher ist viel schneller zugänglich. Die Informationen über die einzelnen Knoten eines NUMA-Systems führt der Kernel in KNODE-Datenstrukturen. Die Kernelvariable KeNodeBlock ist ein Array aus Zeigern auf die KNODE-Strukturen der einzelnen Knoten. Das KNODE-Format können Sie mit dem Kerneldebuggerbefehl dt wie folgt ermitteln: lkd> dt nt!_KNODE +0x000 IdleNonParkedCpuSet : Uint8B +0x008 IdleSmtSet : Uint8B +0x010 IdleCpuSet : Uint8B +0x040 DeepIdleSet : Uint8B +0x048 IdleConstrainedSet : Uint8B +0x050 NonParkedSet : Uint8B +0x058 ParkLock : Int4B +0x05c Seed : Uint4B +0x080 SiblingMask : Uint4B +0x088 Affinity : _GROUP_AFFINITY +0x088 AffinityFill : [10] UChar +0x092 NodeNumber : Uint2B +0x094 PrimaryNodeNumber : Uint2B +0x096 Stride : UChar +0x097 Spare0 : UChar +0x098 SharedReadyQueueLeaders : Uint8B +0x0a0 ProximityId : Uint4B +0x0a4 Lowest : Uint4B +0x0a8 Highest : Uint4B +0x0ac MaximumProcessors : UChar +0x0ad Flags : _flags +0x0ae Spare10 : UChar +0x0b0 HeteroSets : [5] _KHETERO_PROCESSOR_SET Experiment: NUMA-Informationen anzeigen Die Informationen, die Windows über die einzelnen Knoten eines NUMA-Systems führt, können Sie mit dem Kerneldebuggerbefehl !numa einsehen. Wenn Sie keine NUMA-Hardware haben, können Sie zur Durchführung dieses Experiments in Hyper-V eine virtuelle Maschine so einrichten, dass sie mehr als einen NUMA-Knoten verwendet. Allerdings benötigen Sie dazu einen Hostcomputer mit mehr als vier logischen Prozessoren. Gehen Sie wie folgt vor: → Threadplanung Kapitel 4 303
1. Klicken Sie auf Start, geben Sie hyper ein und klicken Sie auf Hyper-V-Manager. 2. Vergewissern Sie sich, dass die VM ausgeschaltet ist, da Sie die folgenden Änderungen sonst nicht vornehmen können. 3. Rechtsklicken Sie im Hyper-V-Manager auf die VM und wählen Sie Einstellungen. 4. Klicken Sie auf Arbeitsspeicher und deaktivieren Sie Dynamischer Arbeitsspeicher. 5. Klicken Sie auf Prozessor und geben Sie 4 in das Feld Anzahl virtueller Prozessoren ein. 6. Erweitern Sie Prozessor und wählen Sie NUMA. 7. Geben Sie in die Felder Maximale Prozessoranzahl und Maximale Anzahl von NUMA-­ Knoten in einem Socket jeweils 2 ein. 8. Klicken Sie auf OK, um die Einstellungen zu speichern. 9. Starten Sie die VM. 10. Geben Sie in einem Kerneldebugger den Befehl !numa ein. Die folgende Ausgabe stammt von der in diesem Experiment eingerichteten VM: 2: kd> !numa NUMA Summary: -----------Number of NUMA nodes : 2 Number of Processors : 4 unable to get nt!MmAvailablePages MmAvailablePages : 0x00000000 KeActiveProcessors : ****---------------------------- (0000000f) → 304 Kapitel 4 Threads
NODE 0 (FFFFFFFF820510C0): Group : 0 (Assigned, Committed, Assignment Adjustable) ProcessorMask : **------------------------------ (00000003) ProximityId : 0 Capacity : 2 Seed : 0x00000001 IdleCpuSet : 00000003 IdleSmtSet : 00000003 NonParkedSet : 00000003 Unable to get MiNodeInformation NODE 1 (FFFFFFFF8719E0C0): Group : 0 (Assigned, Committed, Assignment Adjustable) ProcessorMask : --**---------------------------- (0000000c) ProximityId : 1 Capacity : 2 Seed : 0x00000003 IdleCpuSet : 00000008 IdleSmtSet : 00000008 NonParkedSet : 0000000c Unable to get MiNodeInformation Anwendungen, die aus einem NUMA-System die bestmögliche Leistung herausholen wollen, bemühen sich, einen Prozess auf die Prozessoren eines bestimmten Knotens zu beschränken, indem sie die Affinitätsmaske entsprechend festlegen. Allerdings beschränkt Windows aufgrund seines NUMA-fähigen Threadplanungsalgorithmus ohnehin fast alle Threads auf einen einzigen NUMA-Knoten. Wie die Planungsalgorithmen die Besonderheiten eines NUMA-Systems berücksichtigen, beschreiben wir weiter hinten in diesem Kapitel im Abschnitt »Prozessorauswahl«. Die Optimierungen, mit denen der Speicher-Manager die Vorteile des lokalen Knotenspeichers nutzt, werden in Kapitel 5 behandelt. Prozessorgruppenzuweisung Windows fragt die Topologie des Systems ab, um die Beziehungen zwischen den logischen Prozessoren, SMT-Sätzen, Mehrkernpaketen und physischen Sockeln aufzubauen. Dabei weist es die Prozessoren ihren Gruppen zu, die zur Beschreibung der Affinität herangezogen werden (durch die bereits vorgestellte erweiterte Affinitätsmaske). Diese Arbeit erledigt die Routine KePerformGroupConfiguration, die bei der Initialisierung noch vor jeglichen anderen Arbeiten in Phase 1 aufgerufen wird. Der Vorgang läuft in folgenden Schritten ab: 1. Die Funktion fragt alle erkannten Knoten ab (KeNumberNodes) und berechnet jeweils deren Kapazität, also die Anzahl der logischen Prozessoren, die sie aufnehmen können. Dieser Wert wird im Array KeNodeBlock, das alle NUMA-Knoten auf dem System beschreibt, in Threadplanung Kapitel 4 305
MaximumProcessors gespeichert. Ermöglicht das System die Verwendung von NUMA-­NäheIDs, so wird auch die Nähe-ID eines jeden Knotens abgefragt und in dem Knotenblock gespeichert. 2. Das NUMA-Abstandsarray wird zugewiesen (KeNodeDistance) und die Abstände zwischen den einzelnen NUMA-Knoten werden berechnet. Die nächsten Schritte betreffen Benutzerkonfigurationsoptionen, die die NUMA-Standardzuordnungen überschreiben können. Stellen Sie sich beispielsweise ein System mit Hyper-V vor, auf dem der Hypervisor für den automatischen Start eingerichtet ist. Wenn die CPU die erweiterte Hypervisorschnittstelle nicht unterstützt, wird nur eine Prozessorgruppe aktiviert. Alle NUMA-Knoten (so viele, wie passen) werden Gruppe 0 zugeordnet. In diesem Fall kann Hyper-V daher die Vorteile von Computern mit mehr als 64 Prozessoren nicht nutzen. 3. Die Funktion prüft, ob Daten für statische Gruppenzuweisungen vom Lader übergeben wurden (was bedeutet, dass sie vom Benutzer konfiguriert wurden). Diese Daten geben die Näheinformationen und die Gruppenzuordnungen für die einzelnen NUMA-Knoten an. Wenn Sie auf großen NUMA-Servern eine genauere Kontrolle über die Näheinformationen und Gruppenzuordnungen benötigen, etwa für Tests oder Validierungen, können Sie diese Daten über die Registrierungswerte Group Assignment und Node Distance im Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\NUMA festlegen. Das Format dieser Daten schließt einen Zähler ein, auf den das Array mit den Nähe-IDs und Gruppenzuordnungen folgt. Die Arrayelemente sind allesamt 32-Bit-Werte. 4. Bevor der Kernel die Daten als gültig einstuft, vergleicht er die Nähe-ID mit der Knotennummer. Anschließend ordnet er die Gruppennummern wie verlangt zu. Er sorgt dafür, dass der NUMA-Knoten 0 der Gruppe 0 zugeordnet wird und dass die Kapazität aller NUMA-Knoten der Gruppengröße entspricht. Schließlich prüft die Funktion, wie viele ­ Gruppen noch Kapazitäten frei haben. Der NUMA-Knoten 0 wird unter allen Umständen Gruppe 0 zugeordnet. 5. 306 Der Kernel versucht, die NUMA-Knoten dynamisch zu den Gruppen zuzuweisen, wobei er jedoch jegliche statischen Konfigurationen berücksichtigt, die ihm wie zuvor beschrieben übergeben wurden. Normalerweise bemüht sich der Kernel, die Anzahl der Gruppen zu minimieren, indem er jeweils so viele NUMA-Knoten wie möglich in eine Gruppe aufnimmt. Wenn dieses Verhalten nicht erwünscht ist, können Sie es jedoch mit dem Laderparameter /MAXGROUP ändern, der über die BCD-Option maxgroup eingestellt wird. Damit wird das Standardverhalten überschrieben, sodass der Algorithmus nun versucht, so viele NUMA-Knoten wie möglich auf so viele Gruppen wie möglich zu verteilen, wobei jedoch ein Grenzwert von maximal 20 Gruppen gilt. Gibt es nur einen Knoten oder passen alle Knoten in eine einzige Gruppe (und ist maxgroup ausgeschaltet), ordnet das System alle Knoten standardmäßig der Gruppe 0 zu. Kapitel 4 Threads
6. Bei mehr als einem Knoten prüft Windows die statischen NUMA-Knotenabstände (falls vorhanden) und sortiert die Knoten nach ihrer Kapazität, wobei die größten Knoten am Anfang stehen. Im Modus mit Minimierung der Gruppenanzahl ermittelt der Kernel die maximale Anzahl der möglichen Prozessoren, indem er alle Kapazitäten addiert. Das Ergebnis teilt der Kernel durch die Anzahl der Prozessoren pro Gruppe und erhält dadurch die Gesamtanzahl der Gruppen auf dem Computer (die allerdings auf einen Höchstwert von 20 begrenzt ist). Im Modus mit Maximierung der Gruppenanzahl geht der Kernel zunächst davon aus, dass es ebenso viele Gruppen wie Knoten gibt (wiederum bis zum Höchstwert von 20). 7. Der Kernel beginnt mit dem endgültigen Zuordnungsvorgang. Alle früheren festen Zuordnungen werden jetzt bestätigt, und die Gruppen für diese Zuordnungen werden erstellt. 8. Alle NUMA-Knoten werden neu gemischt, um die Abstände zwischen den Knoten in einer Gruppe zu minimieren. Das heißt, nah beieinander stehende Knoten werden in dieselbe Gruppe gestellt und nach Abstand geordnet. 9. Der gleiche Vorgang wird für alle dynamischen Zuordnungen von Knoten zu Gruppen durchgeführt. 10. Alle restlichen leeren Knoten werden Gruppe 0 zugeordnet. Anzahl der logischen Prozessoren pro Gruppe Gewöhnlich weist Windows einer Gruppe 64 Prozessoren zu. Dies können Sie jedoch mithilfe der Laderoptionen /GROUPSIZE ändern, die über das BCD-Element groupsize eingestellt wird. Durch die Angabe einer Zweierpotenz können Sie dafür sorgen, dass die Gruppen weniger Prozessoren als üblich enthalten, etwa um die Gruppennutzung des Systems zu testen. Auf einem System mit acht logischen Prozessoren können Sie dadurch eine, zwei oder vier Gruppen einrichten. Darüber hinaus können Sie mit der Option /FORCEGROUPAWARE (BCD-Element groupaware) dafür sorgen, dass der Kernel Gruppe 0 nach Möglichkeit vermeidet, sondern stattdessen bei Tätigkeiten wie der Affinitätsauswahl für Threads und DPCs und die Gruppenzuweisung von Prozessen die höchste verfügbare Gruppennummer vergibt. Vermeiden Sie es aber, eine Gruppengröße von 1 anzugeben, da Sie dadurch praktisch alle Anwendungen auf dem System dazu zwingen würden, sich so zu verhalten, als liefen sie auf einem Einzelprozessorcomputer. Das liegt daran, dass der Kernel die Affinitätsmaske eines Prozesses bei dieser Einstellung so einrichtet, dass sie nur eine einzige Gruppe einschließt, sofern die Anwendung nichts anderes verlangt (was die meisten nicht tun). Wenn die Anzahl der logischen Prozessoren auf einem Paket nicht in eine einzige Gruppe passt, ändert Windows diese Anzahl, um sie passend zu machen. Dazu verkleinert es den Wert von CoresPerPhysicalProcessor und, falls der SMT-Satz nicht passt, auch den von LogicalProcessorsPerCore. Eine Ausnahme besteht, wenn das System mehrere NUMA-Knoten in einem Paket enthält (was zwar unüblich, aber durchaus möglich ist). In solchen Multichipmodulen (MCMs) befinden sich zwei Sätze von Kernen sowie zwei Speichercontroller auf demselben Die/Paket. Wenn die ACPI-SRAT (statische Ressourcenaffinitätstabelle) angibt, dass das MCM über zwei NUMA-Knoten verfügt, kann Windows die beiden Knoten zwei Threadplanung Kapitel 4 307
verschiedenen Gruppen zuordnen (was vom Gruppenkonfigurationsalgorithmus abhängt). In einem solchen Fall ist es möglich, dass das MCM-Paket mehr als eine Gruppe überspannt. Diese Optionen können erhebliche Treiber- und Anwendungskompatibilitätsprobleme hervorrufen. Tatsächlich sind sie zur Verwendung durch Entwickler gedacht, um solche Probleme zu erkennen und auszumerzen. Darüber hinaus haben diese Optionen jedoch noch eine starke Auswirkung auf den Computer, denn sie erzwingen NUMA-Verhalten auch auf Nicht-NUMA-Rechnern. Wie Sie im Zuordnungsalgorithmus gesehen haben, lässt Windows es niemals zu, dass ein NUMA-Knoten mehrere Gruppen überspannt. Wenn der Kernel nun aber künstlich kleine Gruppen erstellt, müssen die Gruppen jeweils über ihren eigenen NUMA-Knoten verfügen. Beispielsweise werden auf einem Vierkernprozessor mit der Gruppengröße 2 zwei Gruppen und damit auch zwei NUMA-Knoten als Unterknoten des Hauptknotens erstellt. Das beeinflusst die Richtlinien zur Threadplanung und Speicherverwaltung auf die gleiche Weise wie eine echte NUMA-Architektur, was für Testzwecke praktisch sein kann. Status der logischen Prozessoren Neben den gemeinsamen und lokalen Bereitschaftswarteschlangen und -übersichten unterhält Windows die beiden folgenden zwei Bitmasken, die den Status der Prozessoren auf dem System verfolgen (siehe den Abschnitt »Prozessorauswahl« weiter hinten): QQ KeActiveProcessors Dies ist die Maske der aktiven Prozessoren, in der für jeden verwendbaren Prozessor auf dem System ein Bit gesetzt wird. Dies können weniger Prozessoren sein, als der Computer tatsächlich hat, wenn die Lizenzierungsgrenzwerte von Windows nur eine geringere Anzahl von Prozessoren zulassen. Anhand der Variablen KeRegisteredProcessors können Sie nachschauen, wie viele Prozessoren auf dem Computer tatsächlich lizenziert sind. Mit dem Begriff »Prozessor« sind hier die physischen Pakete gemeint. QQ KeMaximumProcessors Dies ist die Höchstzahl an gebundenen logischen Prozessoren innerhalb der Lizenzierungsgrenzen (einschließlich aller möglichen zukünftigen dynamischen Prozessorergänzungen). Sie gibt auch Plattformbeschränkungen an, die ggf. durch einen Aufruf der HAL und die Überprüfung der ACPI-SRAT abgefragt werden können. Zu den Knotendaten (KNODE) gehören auch Angaben über die im Leerlauf befindlichen CPUs in dem Knoten (IdleCpuSet), über die nicht geparkten CPUs (IdleNonParkedCpuSet) und über SMT-Sätze im Leerlauf (IdleSmtSet). Skalierbarkeit des Schedulers Auf einem Mehrprozessorsystem kann es vorkommen, dass ein Prozessor Datenstrukturen für die Threadplanung eines anderen Prozessors ändern muss, etwa wenn er einen Thread einfügt, der nur auf einem bestimmten Prozessor laufen kann. Aus diesem Grund werden die Strukturen durch Warteschlangen-Spinlocks auf der Ebene DISPATCH_LEVEL für die einzelnen PRCBs synchronisiert. Die Threadauswahl kann daher weitergehen, während nur der PRCB des Prozessors gesperrt ist. Bei Bedarf kann auch der PRCB eines weiteren Prozessors gesperrt werden, etwa beim Threaddiebstahl (siehe die Erklärung weiter hinten). Threadkontextwechsel werden auch durch feinere Spinlocks für einzelne Threads synchronisiert. 308 Kapitel 4 Threads
Es gibt auch für jede CPU eine Liste von Threads im Status Verzögert bereit (DeferredReadyListHead). Diese Threads sind zum Laufen bereit, wurden aber noch nicht zur Ausführung fertig gemacht. Die tatsächliche Bereitstellungsoperation wurde auf einen späteren Zeitpunkt verschoben. Da jeder Prozessor seine eigene Liste von Threads mit verzögerter Bereitschaft führt, wird sie nicht von dem PRCB-Spinlock synchronisiert. Sie wird von KiProcessDeferredReadyList verarbeitet, nachdem eine Funktion bereits Änderungen an der Prozess- oder Threadaffinität, an der Priorität (einschließlich Prioritätserhöhungen) oder den Quantumswerten vorgenommen hat. Diese Funktion ruft für jeden Thread auf der Liste KiDeferredReadyThread auf, die den weiter hinten im Abschnitt »Prozessorauswahl« vorgestellten Algorithmus durchführt. Dadurch kann es dazu kommen, dass der Thread sofort ausgeführt, in die Bereitschaftsliste des Prozessors aufgenommen oder, falls der Prozessor unerreichbar ist, auf die Verzögert-bereit-Liste eines anderen Prozessors gesetzt wird, wo er in den Zustand Standby übergehen oder sofort ausgeführt werden kann. Diese Eigenschaft wird von der Parking-Engine beim Parken eines Kerns genutzt: Alle Threads werden auf die Verzögert-bereit-Liste gesetzt, die dann verarbeitet wird. Da KiDeferredReadyThread geparkte Kerne überspringt (wie wir noch zeigen werden), landen alle Threads dieses Prozessors daher auf anderen Prozessoren. Affinität Jeder Thread hat eine Affinitätsmaske, die die Prozessoren angibt, auf denen er laufen darf. Diese Maske erbt er von der Prozessaffinitätsmaske. Normalerweise haben alle Prozesse (und daher auch alle Threads) zu Anfang eine Affinitätsmaske, die alle aktiven Prozessoren ihrer Gruppe einschließt. Das System kann die Threads also auf sämtlichen verfügbaren Prozessoren in der ihrem Prozess zugeordneten Gruppe einplanen. Um den Durchsatz zu optimieren, die Arbeitslasten für einen bestimmten Satz von Prozessoren zu partitionieren oder beides, kann eine Anwendung die Affinitätsmaske für einen Thread jedoch ändern. Das kann auf verschiedenen Ebenen geschehen: QQ Durch einen Aufruf von SetThreadAffinityMask wird die Affinität für einen einzelnen Thread festgelegt. QQ Durch einen Aufruf von SetProcessAffinityMask wird die Affinität für alle Threads in einem Prozess festgelegt. Task-Manager und Process Explorer bieten eine grafische Oberfläche für diese Aufgabe an. Dazu rechtsklicken Sie auf einen Prozess und wählen Zugehörigkeit festlegen bzw. Set Affinity. Im Sysinternals-Werkzeug Psexec gibt es eine Befehlszeilenschnittstelle dafür (siehe den Schalter -a in der Ausgabe der Hilfe). QQ Wenn der Prozess zu einem Job gehört, können Sie eine jobweite Affinitätsmaske mit der Funktion SetInformationJobObject festlegen (siehe Kapitel 3). QQ Beim Kompilieren der Anwendung können Sie eine Affinitätsmaske im Abbildheader festlegen. Threadplanung Kapitel 4 309
Eine ausführliche Spezifikation des Windows-Abbildformats können Sie sich ansehen, indem Sie auf http://msdn.microsoft.com nach Portable Executable and Common Object File Format Specification suchen. Bei einem Abbild kann zur Verlinkungszeit auch das Flag uniprocessor gesetzt werden. Wenn das der Fall ist, wählt das System bei der Prozesserstellung einen einzelnen Prozessor aus (MmRotatingProcessorNumber) und weist diesen in der Prozessaffinitätsmaske zu. Dabei beginnt das System mit dem ersten Prozessor und durchläuft dann alle Prozessoren in der Gruppe. Wird beispielsweise auf einem Zwei-Prozessor-System ein mit dem Flag uniprocessor gekennzeichnetes Abbild zum ersten Mal gestartet, so wird ihm CPU 0 zugewiesen, beim zweiten Mal CPU 1, beim dritten Mal CPU 0, beim vierten Mal CPU 1 usw. Dieses Flag bildet eine praktische Notlösung für Programme mit Bugs bei der Multithreadsynchronisierung, die auf Mehrprozessorsystemen aufgrund von Race Conditions auftreten, aber nicht auf Einzelprozessorsystemen. Wenn ein Abbild solche Symptome zeigt und nicht signiert ist, können Sie das Flag manuell hinzufügen, indem Sie den Abbildheader mit einem PE-Bearbeitungswerkzeug ändern. Eine bessere Lösung, die sich auch für signierte ausführbare Dateien eignet, besteht darin, mit dem Microsoft Application Compatibility Toolkit ein Shim hinzuzufügen, das die Kompatibilitätsdatenbank zwingt, das Abbild zur Startzeit als reine Einzelprozessoranwendung zu kennzeichnen. Experiment: Die Prozessaffinität einsehen und ändern In diesem Experiment ändern Sie die Affinitätseinstellungen für einen Prozess und beobachten, wie diese Affinität von neuen Prozessen geerbt wird. 1. Starten Sie die Eingabeaufforderung (Cmd.exe). 2. Starten Sie den Task-Manager oder Process Explorer und machen Sie Cmd.exe in der Prozessliste ausfindig. 3. Rechtsklicken Sie auf den Prozess und wählen Sie Zugehörigkeit festlegen bzw. Set Affinity. Daraufhin wird eine Liste der Prozessoren angezeigt. Auf einem System mit acht logischen Prozessoren sehen Sie beispielsweise Folgendes: → 310 Kapitel 4 Threads
4. Wählen Sie einen Teil der verfügbaren Prozessoren auf dem System aus und klicken Sie auf OK. Die Threads des Prozesses können jetzt nur auf den ausgewählten Prozessoren ausgeführt werden. 5. Geben Sie an der Eingabeaufforderung Notepad ein, um den Editor zu starten. 6. Suchen Sie im Task-Manager oder in Process Explorer den Prozess Notepad. 7. Rechtsklicken Sie auf den Prozess und wählen Sie Zugehörigkeit bzw. Affinity. Es werden jetzt die Prozessoren angezeigt, die Sie für den Prozess der Eingabeaufforderung ausgewählt haben, da Prozesse ihre Affinitätseinstellungen vom Elternprozess erben. Wenn ein laufender Thread auch auf einem anderen Prozessor ausgeführt werden könnte, verschiebt Windows ihn jedoch trotzdem nicht, um einem Thread Platz zu machen, der nur auf dem von ihm belegten Prozessor laufen kann. Nehmen wir an, auf CPU 0 läuft ein Thread der Priorität 8, der auf einem beliebigen Prozessor ausgeführt werden kann, und auf CPU 1 ein Thread der Priorität 4, der ebenfalls keinen Prozessoreinschränkungen unterliegt. Nun wird ein Thread der Priorität 6, der nur auf CPU 0 laufen darf, zur Ausführung bereit. Was geschieht? Der Priorität-8-Thread wird nicht von CPU 0 zu CPU 1 verschoben (wodurch der Priorität-4-Thread vorzeitig unterbrochen würde), damit der Priorität-6-Thread laufen kann. Letzterer bleibt weiterhin im Zustand Bereit. Eine Änderung der Affinitätsmaske für einen Prozess oder Thread kann daher dazu führen, dass den Threads weniger CPU-Zeit zur Verfügung steht als normal, da Windows den Thread nur auf bestimmten Prozessoren ausführen kann. Daher müssen Sie bei der Festlegung von Affinitäten äußerste Vorsicht walten lassen. Meistens ist es am besten, Windows entscheiden zu lassen, welche Threads wo laufen sollen. Erweiterte Affinitätsmaske Um mehr als die 64 Prozessoren unterstützen zu können, die bei der ursprünglichen Struktur der Affinitätsmaske (64 Bits auf einem 64-Bit-System) möglich sind, verwendet Windows die erweiterte Affinitätsmaske KAFFINITY_EX. Dabei handelt es sich um ein Array aus Affinitätsmasken für die einzelnen Prozessorgruppen (zurzeit maximal 20). Wenn der Scheduler auf einen Prozessor in der erweiterten Affinitätsmaske verweisen muss, bestimmt er zunächst die zugehörige Bitmaske anhand der Gruppennummer und greift dann direkt auf die Affinität zu. In der Kernel-API werden die erweiterten Affinitätsmasken nicht verfügbar gemacht. Stattdessen übergibt der Aufrufer der API die Gruppennummer als Parameter und erhält die klassische Affinitätsmaske für die Gruppe zurück. In der Windows-API dagegen können gewöhnlich nur Informationen über eine einzelne Gruppe abgefragt werden, nämlich die Gruppe des zurzeit laufenden Threads (die fest ist). Die erweiterte Affinitätsmaske und ihre zugrunde liegenden Mechanismen bieten einem Prozess auch die Möglichkeit, die Grenzen der Prozessorgruppe zu überschreiten, der er ursprünglich zugewiesen wurde. Durch die Verwendung von APIs für erweiterte Affinität können die Threads in einem Prozess Affinitätsmasken anderer Prozessorgruppen auswählen. Stellen Sie sich einen Computer mit 256 Prozessoren und dort einen Prozess mit vier Threads vor. Wenn die Threads nun alle die Affinitätsmaske 0x10 (binär 0b10000) verwenden, aber jeweils Threadplanung Kapitel 4 311
für eine andere Gruppe, dann kann Thread 1 auf Prozessor 4 laufen, 2 auf 68, 3 auf 132 und 4 auf 196. Es ist auch möglich, dass alle Bits der Affinitätsmasken für die gegebenen Gruppen auf 1 gesetzt werden (0xFFFF...), sodass der Prozess seine Threads auf allen verfügbaren Prozessoren des Systems ausführen kann (wobei jeder Thread jedoch nur auf Prozessoren seiner eigenen Gruppe laufen darf). Um die Vorteile dieser erweiterten Affinität zu nutzen, können Sie beim Erstellen eines neuen Threads oder beim Aufrufen von SetThreadGroupAffinity für einen vorhandenen Thread eine Gruppennummer in der Threadattributliste angeben (PROC_THREAD_ATTRIBUTE_GROUP_­AFFINITY). Die Systemaffinitätsmaske Windows-Treiber werden gewöhnlich im Kontext des aufrufenden oder eines willkürlichen Threads ausgeführt (also nicht in der sicheren Begrenzung des Systemprozesses). Daher kann der zurzeit laufende Treibercode den vom Entwickler der Anwendung festgelegten Affinitätsregeln unterworfen sein, auch wenn diese Regeln für den Treibercode eigentlich nicht von Bedeutung sind und ihn sofort von der richtigen Verarbeitung von Interrupts und anderen Arbeiten in der Warteschlange abhalten können. Treiberentwicklern steht daher mit den APIs KeSetSystemAffinityThread(Ex)/KeSetSystemGroupAffinityThread und KeRevertToUser-AffinityThread(Ex)/KeRevertToUserGroupAffinityThread ein Mechanismus zur Verfügung, um die Affinitätseinstellungen für den Benutzerthread vorübergehend zu umgehen. Idealer und letzter Prozessor Bei jedem Thread sind im Kernel-Threadsteuerungsblock drei CPU-Nummern gespeichert: QQ Idealer Prozessor Dies ist der bevorzugte Prozessor, auf dem der Thread laufen sollte. QQ Letzter Prozessor Dies ist der Prozessor, auf dem der Thread beim letzten Mal ausgeführt wurde. QQ Nächster Prozessor Dies ist der Prozessor, auf dem der Thread ausgeführt werden wird oder bereits läuft. Der ideale Prozessor wird beim Erstellen des Threads mithilfe eines Grundwerts im Prozesssteuerungsblock ausgewählt. Dieser Grundwert wird jedes Mal erhöht, wenn ein Thread angelegt wird, sodass der Wert nach und nach alle verfügbaren Prozessoren des Systems durchläuft. Beispielsweise wird dem ersten Thread im ersten Prozess auf dem System der Wert 0 für den idealen Prozessor zugewiesen, dem zweiten Thread der Wert 2 usw. Dadurch werden die Threads eines Prozesses gleichmäßig über die Prozessoren verteilt. Auf SMT-Systemen (Hyperthread­ ing) wird der nächste ideale Prozessor aus dem nächsten SMT-Satz ausgewählt. Beispielsweise können die Threads in einem Prozess auf einem Quadcore-System mit Hyperthreading die idealen Prozessoren 0, 2, 4, 6, 0, ...; 3, 5, 7, 1, 3, ... usw. bekommen. Dadurch werden die Threads eines Prozesses gleichmäßig über die physischen Prozessoren verteilt. Dabei wird jedoch vorausgesetzt, dass die Threads in einem Prozess jeweils die gleiche Menge Arbeit verrichten. Meistens jedoch enthält ein Prozess einen oder mehrere »Haushaltungs-« und zahlreiche Arbeitsthreads. Um die Vorteile der Mehrprozessorplattform voll auszuschöpfen, kann für eine Multithreadanwendung daher von Vorteil sein, die idealen Prozessoren 312 Kapitel 4 Threads
für ihre Threads mit der Funktion SetThreadIdealProcessor festzulegen. Um auch die Vorteile von Prozessorgruppen zu nutzen, sollten Entwickler stattdessen SetThreadIdealProcessorEx aufrufen, bei der es möglich ist, eine Gruppennummer für die Affinität auszuwählen. In 64-Bit-Versionen von Windows wird das Feld Stride in der KNODE-Struktur verwendet, um die Zuordnung neu erstellter Threads in einem Prozess auszugleichen. Dieser »Schritt« gibt die Anzahl der Affinitätsbits an, die in einem gegebenen NUMA-Knoten übersprungen werden müssen, um einen neuen unabhängigen Bereich logischer Prozessoren zu erreichen. Dabei bedeutet »unabhängig«, dass sich diese Prozessoren in einem anderen Kern (auf SMT-­ Systemen) oder einem anderen Paket (bei Mehrkernsystemen ohne SMT) befinden müssen. Da 32-Bit-Windows-Versionen ohnehin keine Systeme mit umfangreicher Prozessorausstattung unterstützen, verwenden sie keinen Schrittwert. Stattdessen wählen sie einfach den Prozessor der nächsten Nummer aus, wobei sie jedoch nach Möglichkeit vermeiden, dabei wieder denselben SMT-Satz zu verwenden. Der ideale Knoten Wenn auf einem NUMA-System ein Prozess erstellt wird, so wird für ihn ein idealer Knoten ausgewählt. Der erste Prozess wird Knoten 0 zugewiesen, der zweite Knoten 1 usw. Die idealen Prozessoren für die Threads in dem Prozess werden dann unter denen im idealen Knoten ausgewählt. Der erste Thread erhält den ersten Prozessor des Knotens usw. CPU-Sätze Die Affinität (die manchmal auch als harte Affinität bezeichnet wird) schränkt Threads auf bestimmte Prozessoren ein, was der Scheduler stets berücksichtigt. Der Mechanismus, der versucht, Threads auf ihrem idealen Prozessor auszuführen (zuweilen auch weiche Affinität genannt) erwartet im Allgemeinen, dass sich der Status des Threads im Cache dieses Prozessors befindet. Es ist jedoch nicht garantiert, dass der ideale Prozessor verwendet wird; der Thread kann auch auf anderen Prozessoren eingeplant werden. Beide Mechanismen gelten nicht für Systemaktivitäten wie die von Systemthreads. Es gibt auch keine einfache Möglichkeit, die harte Affinität für alle Prozesse auf einem System auf einmal festzulegen. Systemprozesse sind im Allgemeinen gegen externe Affinitätsänderungen geschützt, da dafür das Zugriffsrecht PROCESS_SET_INFORMATION erforderlich ist, das für geschützte Prozesse nicht gewährt wird. In Windows 10 und Windows Server 2016 wurden CPU-Sätze eingeführt. Dabei handelt es sich um eine Form von Affinität, die Sie auch für das System als Ganzes (einschließlich Systemthreadaktivitäten), für Prozesse und sogar für einzelne Threads einrichten können. Beispielsweise kann es sich anbieten, dass eine Audioanwendung mit geringer Latenz einen Prozessor ganz für sich allein nutzt, während der Rest des Systems auf andere Prozessoren umgeleitet wird. Das lässt sich mithilfe von CPU-Sätzen erreichen. Zurzeit sind die Möglichkeiten der dokumentierten Benutzermodus-API recht eingeschränkt. GetSystemCpuSetInformation gibt ein Array mit SYSTEM_CPU_SET_INFORMATION-Strukturen zurück, die Daten über die einzelnen CPU-Sätze enthalten. In der aktuellen Implementierung entspricht ein CPU-Satz einer einzelnen CPU. Daher entspricht die Länge des zurückgegebenen Arrays der Anzahl logischer Prozessoren auf dem System. Jeder CPU-Satz wird durch seine ID Threadplanung Kapitel 4 313
bezeichnet, die sich aus 256 (0x100) und dem CPU-Index (0, 1, ...) zusammensetzt. Dies sind die IDs, die Sie an SetProcessDefaultCpuSets und SetThreadSelectedCpuSets übergeben müssen, um die CPU-Standardsätze für einen Prozess bzw. einen CPU-Satz für einen bestimmten Thread festzulegen. Einen CPU-Satz für einen Thread legen Sie beispielsweise fest, wenn der Thread so wichtig ist, dass er nach Möglichkeit nicht unterbrochen werden soll. Diesem Thread können Sie einen CPU-Satz mit einer einzigen CPU zuordnen, während Sie in den CPU-Standardsatz für den Prozess alle anderen CPUs aufnehmen. In der Windows-API fehlt jedoch die Möglichkeit, den System-CPU-Satz zu verkleinern. Dies können Sie mit dem Systemaufruf NtSetSystemInformation erledigen, für den der Aufrufer das Recht SeIncreaseBasePriorityPrivilege benötigt. Experiment: CPU-Sätze In diesem Experiment schauen Sie sich CPU-Sätze an, ändern sie und beobachten die Auswirkungen. 1. Laden Sie das Werkzeug CpuSet.exe von den Begleitressourcen zu diesem Buch herunter. 2. Öffnen Sie eine Eingabeaufforderung mit Administratorrechten und wechseln Sie dort zu dem Verzeichnis, in dem sich CpuSet.exe befindet. 3. Geben Sie in der Befehlszeile cpuset.exe ohne Argumente ein, um sich die aktuellen CPU-Sätze des Systems anzusehen. Sie erhalten dabei eine Ausgabe wie die folgende: System CPU Sets --------------Total CPU Sets: 8 CPU Set 0 Id: 256 (0x100) Group: 0 Logical Processor: 0 Core: 0 Last Level Cache: 0 NUMA Node: 0 Flags: 0 (0x0) Parked: False Allocated: False Realtime: False Tag: 0 CPU Set 1 Id: 257 (0x101) Group: 0 Logical Processor: 1 Core: 0 Last Level Cache: 0 → 314 Kapitel 4 Threads
NUMA Node: 0 Flags: 0 (0x0) Parked: False Allocated: False Realtime: False Tag: 0 ... 4. Starten Sie CpuStres.exe und lassen Sie das Programm einen oder zwei Threads mit Aktivitätsgrad Maximum ausführen. Versuchen Sie, eine CPU-Auslastung von ca. 25 % zu erreichen. 5. Öffnen Sie den Task-Manager, rufen Sie die Registerkarte Leistung auf und wählen Sie CPU. 6. Ändern Sie die grafische CPU-Ansicht, sodass statt der Gesamtnutzung die einzelnen Prozessoren angezeigt werden. 7. Führen Sie an der Eingabeaufforderung den folgenden Befehl aus, wobei Sie die Zahl hinter -p durch die Prozess-ID von CPUSTRES auf Ihrem System ersetzen müssen: CpuSet.exe -p 18276 -s 3 Das Argument -s gibt die Prozessormaske an, die als Standard für den Prozess eingerichtet werden soll. Der Wert 3 in diesem Beispiel steht für CPU 0 und 1. Im Task-Manager können Sie jetzt ein hohes Arbeitsaufkommen auf diesen beiden CPUs erkennen: → Threadplanung Kapitel 4 315
8. Als Nächstes sehen wir uns CPU 0 genauer an, um herauszufinden, welche Threads dort laufen. Dazu verwenden Sie den Windows Performance Recorder (WPR) und den Windows Performance Analyzer (WPA) aus dem Windows SDK. Klicken Sie auf Start, geben Sie wpr ein und wählen Sie Windows Performance Recorder. Bestätigen Sie die Rechteerhöhung. Jetzt wird folgendes Dialogfeld angezeigt: 9. In der Standardeinstellung wird die CPU-Nutzung aufgezeichnet, was auch genau das ist, was wir tun wollen. Dieses Werkzeug zeichnet ETW-Ereignisse auf (Event Tracking for Windows, siehe Kapitel 8 in Band 2). Klicken Sie in dem Dialogfeld auf die Schaltfläche Start und nach ein oder zwei Sekunden, wenn sie mittlerweile die Bezeichnung Save beträgt, erneut darauf. 10. WPR schlägt einen Speicherort für die aufgezeichneten Daten vor. Akzeptieren Sie ihn oder geben Sie einen Datei- oder Ordnernamen nach Ihrem Geschmack an. 11. Wenn die Datei gespeichert ist, schlägt Ihnen WPR vor, sie mit WPA zu öffnen. Akzeptieren Sie diesen Vorschlag. → 316 Kapitel 4 Threads
12. WPA wird geöffnet und lädt die gespeicherte Datei. Dieses Werkzeug ist sehr umfangreich, weshalb wir es in diesem Buch nicht ausführlich beschreiben können. Auf der linken Seite sehen Sie die verschiedenen Kategorien der erfassten Informationen: 13. Erweitern Sie Computation und dann CPU Usage (Precise). 14. Doppelklicken Sie auf das Diagramm Utilization by CPU. Es wird daraufhin im Hauptfenster geöffnet: → Threadplanung Kapitel 4 317
15. Zurzeit sind wir an CPU 0 interessiert. Im nächsten Schritt sorgen Sie dafür, dass auf CPU 0 nur CPUSTRES ausgeführt wird. Dazu erweitern Sie zunächst CPU 0. Daraufhin werden mehrere Prozesse angezeigt. Einer von ihnen, aber beileibe nicht der einzige, ist CPUSTRES. 16. Geben Sie den folgenden Befehl ein, damit das System alle Prozessoren außer dem ersten benutzt. Da dieses System über acht Prozessoren verfügt, hat die komplette Maske den Wert 255 (0xff). Wenn Sie CPU 0 herausnehmen, ergibt sich 254 (0xfe). Ersetzen Sie im folgenden Befehl die Maske durch die passende für Ihr System: CpuSet.exe -s 0xfe 17. Die Ansicht im Task-Manager sollte sich nicht ändern. Schauen wir uns aber CPU 0 noch einmal genau an. Führen Sie dazu abermals WPR aus und zeichnen Sie die Messwerte ein oder zwei Sekunden lang mit den gleichen Einstellungen wie zuvor auf. 18. Öffnen Sie die Ablaufverfolgung in WPA und rufen Sie Utilization by CPU auf. 19. Erweitern Sie CPU 0. Dort wird jetzt fast nur noch CPUSTRES angezeigt, wohingegen Systemprozesse nur noch gelegentlich auftreten. → 318 Kapitel 4 Threads
20. Die in der Spalte CPU Usage (in View) (ms) angegebene Zeit für den Systemprozess ist sehr gering (im Bereich von Mikrosekunden). CPU 0 ist fast ausschließlich für den Prozess CPUSTRES da. 21. Führen Sie CpuSet.exe erneut aus, aber diesmal ohne Argumente. Der erste Satz (CPU 0) wird als Allocated: True gekennzeichnet, da er nicht mehr für den allgemeinen Gebrauch im System zur Verfügung steht, sondern einem bestimmten Prozess zugeordnet ist. 22. Schließen Sie CPU Stress. 23. Geben Sie den folgenden Befehl ein, um den System-CPU-Satz wieder auf die Standardeinstellung zurückzusetzen: Cpuset -s 0 Threadauswahl auf Mehrprozessorsystemen Bevor wir uns weiter mit den Einzelheiten von Mehrprozessorsystemen beschäftigen, wollen wir Ihnen noch einmal einen Überblick über die im Abschnitt »Threadauswahl« vorgestellten Algorithmen verschaffen. Diese Algorithmen setzen entweder die Ausführung des aktuellen Threads fort (wenn kein neuer Kandidat gefunden wurde) oder starten den Leerlaufthread (wenn der aktuelle Thread blockiert ist). Es gibt jedoch mit KiSearchForNewThread noch einen dritten Algorithmus für die Threadauswahl. Er wird verwendet, wenn der aktuelle Thread kurz vor der Blockierung steht, weil er auf ein Objekt warten muss, oder wenn NtDelayExecutionThread aufgerufen wurde, die sogenannte Sleep-API. Hier zeigt sich ein feiner Unterschied zwischen dem üblichen Aufruf von Sleep(21), bei dem der aktuelle Thread bis zum nächsten Tick blockiert wird, und dem zuvor gezeigten Aufruf von SwitchToThread. Bei Sleep(1) wird der im Folgenden erklärte Algorithmus verwendet, bei SwitchToThread dagegen die zuvor vorgeführte Logik. KiSearchForNewThread liest das Feld NextThread, um herauszufinden, ob für diesen Prozessor bereits ein Thread ausgewählt wurde. Wenn ja, überführt die Funktion diesen Thread sofort in den Zustand Wird ausgeführt. Anderenfalls ruft sie zunächst KiSelectReadyThreadEx auf, um einen Thread zu finden und danach die gleichen Schritte wie zuvor auszuführen. Wurde kein Thread gefunden, wird der Prozessor als unbeschäftigt gekennzeichnet (auch wenn der Leerlaufthread noch nicht ausgeführt wird). Außerdem werden die (gemeinsamen) Warteschlangen der anderen logischen Prozessoren untersucht (im Unterschied zu den anderen Standardalgorithmen, die an dieser Stelle aufgeben). Allerdings wird auf diese Untersuchung verzichtet, wenn der Prozessorkern geparkt ist, da es zu bevorzugen ist, dass der Kern in den Parkzustand übergeht, anstatt ihn mit neuer Arbeit beschäftigt zu halten. Außer in diesen beiden Sonderfällen wird jetzt die Schleife für den »Threaddiebstahl« ausgeführt. Der Code schaut sich den aktuellen NUMA-Knoten an und entfernt die unbeschäftigten Prozessoren daraus (da sie keine Threads enthalten, die gestohlen werden könn- Threadplanung Kapitel 4 319
ten). Anschließend sieht er sich die gemeinsame Bereitschaftswarteschlange der aktuellen CPU an und ruft KiSearchForNewThreadOnProcessor in einer Schleife auf. Wird dabei kein Thread gefunden, wird die Affinität auf die nächste Gruppe geändert und die Funktion erneut aufgerufen. Diesmal aber zeigt die Ziel-CPU auf die gemeinsame Warteschlange der nächsten Gruppe statt der aktuellen, sodass der Prozessor den besten ausführungsbereiten Thread in der Warteschlange der anderen Prozessorgruppe finden kann. Findet sich auch dabei kein geeigneter Thread, werden die lokalen Warteschlangen der Prozessoren in der Gruppe auf dieselbe Weise durchsucht. Hat auch dies keinen Erfolg und ist DFSS aktiviert, wird stattdessen ein Thread aus der Leerlauf-Warteschlange des anderen logischen Prozessors auf dem aktuellen Prozessor bereitgestellt, falls das möglich ist. Wurde immer noch kein Kandidat gefunden, versucht der Algorithmus es mit dem logischen Prozessor der nächstniedrigen Nummer. So geht es weiter, bis er alle logischen Prozessoren des aktuellen NUMA-Knotens durchlaufen hat. Danach fährt er mit dem nächstliegenden Knoten fort usw., bis er alle Knoten der aktuellen Gruppe untersucht hat. (Denken Sie daran, dass die Affinität eines Threads in Windows immer nur auf eine Gruppe beschränkt ist.) Wird dadurch immer noch kein geeigneter Thread gefunden, gibt die Funktion NULL zurück. Im Falle eines Wartevorgangs führt der Prozessor den Leerlaufthread aus (wodurch die Leerlaufplanung übersprungen wird). Wurde diese Arbeit bereits vom Leerlaufscheduler ausgeführt, tritt der Prozessor in einen Ruhezustand ein. Prozessorauswahl Wir haben bereits beschrieben, wie Windows einen Thread aussucht, wenn ein logischer Prozessor eine Auswahl treffen oder wenn diese Auswahl für ihn vorgenommen werden muss. Dabei haben wir die Existenz einer Datenbank ausführungsbereiter Threads vorausgesetzt, die zur Auswahl bereitstehen. Jetzt sehen wir uns an, wie diese Datenbank gefüllt wird, also wie Windows den logischen Prozessor auswählt, mit dessen Bereitschaftswarteschlange es den ausführungsbereiten Thread verknüpft. Nachdem Sie bereits die verschiedenen Arten von Mehrprozessorsystemen sowie die Einstellungen für Threadaffinität und den idealen Prozessor kennengelernt haben, schauen wir uns nun an, wie diese Informationen für diesen Zweck genutzt werden. Prozessorauswahl bei Vorhandensein von unbeschäftigten Prozessoren Wird ein Thread ausführungsbereit, so wird die Schedulerfunktion KiDeferredReadyThread aufgerufen. Windows muss dann die beiden folgenden Aufgaben ausführen: QQ Nach Bedarf die Prioritäten anpassen und die Quanten aktualisieren (siehe den Abschnitt »Prioritätserhöhung«) QQ Den geeignetsten logischen Prozessor für den Thread auswählen Windows schlägt als Erstes den idealen Prozessor des Threads nach und ermittelt dann, welche Prozessoren in der Maske für die harte Affinität des Threads unbeschäftigt sind. Diese Menge wird anschließend noch wie folgt verkleinert: 320 Kapitel 4 Threads
1. Alle unbeschäftigten logischen Prozessoren, die geparkt wurden, werden aus der Auswahl entfernt. (Mehr über das Parken von Prozessorkernen erfahren Sie in Kapitel 6.) Bleiben dann keine unbeschäftigten Prozessoren mehr übrig, wird die Auswahl abgebrochen. Der Scheduler verhält sich dann so, als stünden keine unbeschäftigten Prozessoren zur Verfügung (siehe den nächsten Abschnitt). 2. Jegliche unbeschäftigten logischen Prozessoren, die sich nicht im idealen Knoten (also dem Knoten mit dem idealen Prozessor) befinden, werden entfernt (es sei denn, dass dadurch keine unbeschäftigten Prozessoren mehr übrig blieben). 3. Auf einem SMT-System werden alle beschäftigten SMT-Sätze entfernt, auch wenn dadurch der ideale Prozessor selbst wegfällt. Windows bevorzugt also einen nicht idealen unbeschäftigten SMT-Satz gegenüber einem idealen Prozessor. 4. Windows prüft, ob sich der ideale Prozessor unter den übrig gebliebenen unbeschäftigten Prozessoren befindet. Ist das nicht der Fall, muss es den geeignetsten unbeschäftigten Prozessor ermitteln. Dazu sieht es zunächst nach, ob die Auswahl den Prozessor enthält, auf dem der Thread beim letzten Mal ausgeführt wurde. Wenn ja, so wird er als temporärer idealer Prozessor angesehen und ausgewählt. (Durch die Nutzung des idealen Prozessors soll die Anzahl der Prozessorcachetreffer erhöht werden, und dieses Ziel lässt sich durch die Auswahl des zuletzt verwendeten Prozessors gut erreichen.) Befindet sich der zuletzt verwendete Prozessor jedoch nicht in der verbliebenen Auswahl an unbeschäftigten Prozessoren, prüft Windows, ob der aktuelle Prozessor (also derjenige, der zurzeit den Threadplanungscode ausführt) darin enthalten ist. Wenn ja, wird die gleiche Logik angewendet wie zuvor. 5. Ist weder der zuletzt verwendete noch der aktuelle Prozessor unbeschäftigt, entfernt Windows aus der Auswahl noch jegliche unbeschäftigten logischen Prozessoren, die sich nicht im selben SMT-Satz befinden wie der ideale Prozessor. Sind keine solche Prozessoren übrig, entfernt Windows stattdessen jegliche Prozessoren, die sich nicht im SMT-Satz des aktuellen Prozessors befinden (es sei denn, dadurch würden keine unbeschäftigten Prozessoren übrig bleiben). Anders ausgedrückt, Windows bevorzugt unbeschäftigte Prozessoren aus demselben SMT-Satz gegenüber einem nicht erreichbaren idealen oder zuletzt verwendeten Prozessor. Da der Cache bei SMT-Implementierungen im gesamten Kern gemeinsam verwendet wird, hat das für die Anzahl der Cachetreffer etwa die gleichen Auswirkungen wie die Auswahl des idealen oder zuletzt verwendeten Prozessors. 6. Enthält die Auswahl nach dem letzten Schritt noch mehr als einen Prozessor, wählt Windows den mit der niedrigsten Nummer für den Thread aus. Nach der Auswahl des Prozessors wird der Thread in den Standbyzustand versetzt und der PRCB des unbeschäftigten Prozessors so aktualisiert, dass er auf den Thread zeigt. Ist der Prozessor unbeschäftigt, aber nicht angehalten, wird ein DPC-Interrupt gesendet, sodass sich der Prozessor sofort um die Threadplanungsoperation kümmert. Beim Auslösen einer solchen Operation wird immer KiCheckForThreadDispatch ausgewählt. Sie erkennt, dass auf dem Prozessor ein neuer Thread eingeplant wurde, verursacht nach Möglichkeit einen sofortigen Kontextwechsel, informiert Autoboost über den Wechsel und liefert ausstehende APCs aus. Gibt es keinen ausstehenden Thread, sorgt sie stattdessen dafür, dass ein DPC-Interrupt gesendet wird. Threadplanung Kapitel 4 321
Prozessorauswahl ohne Vorhandensein von unbeschäftigten Prozessoren Wenn ein Thread ausgeführt werden will und keine unbeschäftigten, nicht geparkten Prozessoren vorhanden sind, prüft Windows als Erstes, ob einer der geparkten verwendet werden kann. Dazu ruft der Scheduler KiSelectCandidateProcessor auf, um die Parking-Engine nach dem geeignetsten Kandidaten zu fragen. Die Engine wählt den nicht geparkten Prozessor mit der höchsten Nummer im idealen Knoten aus. Gibt es keine solchen Prozessoren, hebt sie den Parkstatus des idealen Prozessors zwangsweise auf. Wenn der Scheduler die Steuerung zurückerhält, prüft er, ob der empfangene Kandidat unbeschäftigt ist. Wenn ja, wählt er diesen Prozessor für den Thread aus, wobei er ebenso vorgeht wie in dem zuvor geschilderten Fall. Wenn das nicht funktioniert, muss Windows entscheiden, ob es den zurzeit laufenden Thread vorzeitig unterbrechen soll. Als Erstes muss es dazu einen Zielprozessor auswählen. Dies geschieht, abgesehen von Affinitätseinschränkungen, in der Reihenfolge der Präferenz: An erster Stelle steht der ideale Prozessor für den Thread, gefolgt vom zuletzt verwendeten Prozessor, dem ersten verfügbaren Prozessor im aktuellen NUMA-Knoten und schließlich der nächstgelegene Prozessor in einem anderen NUMA-Knoten. Nach der Auswahl eines Prozessors stellt sich als Nächstes die Frage, ob der neue Thread denjenigen vorzeitig unterbrechen soll, der zurzeit auf dem Prozessor läuft. Dazu wird der Rang der beiden Threads verglichen. Dies ist ein interner Wert für die Threadplanung, der die relative Bedeutung eines Threads auf der Grundlage seiner Planungsgruppe und anderer Faktoren angibt (siehe den Abschnitt »Gruppengestützte Threadplanung« weiter hinten in diesem Kapitel). Hat der neue Thread den Rang 0 (was den höchsten Rang bezeichnet) oder eine kleinere Rangnummer als der laufende Thread oder hat er den gleichen Rang, aber eine höhere Priorität, so erfolgt eine vorzeitige Unterbrechung. Der laufende Thread wird zur Unterbrechung markiert und Windows stellt einen DPC-Interrupt an den Zielprozessor in die Warteschlange, um den laufenden Thread zugunsten des neuen Threads zu unterbrechen. Wenn der ausführungsbereite Thread nicht sofort ausgeführt werden kann, wird er ( je nach den Affinitätseinschränkungen) in die gemeinsame oder lokale Bereitschaftswarteschlange gestellt, wo er wartet, bis er an der Reihe ist. Wie Sie bei den vorherigen Fällen gesehen haben, wird er je nachdem, ob er aufgrund einer vorzeitigen Unterbrechung in die Warteschlange gestellt wurde, an deren Anfang oder Ende gesetzt. Unabhängig vom vorliegenden Planungsszenario werden Threads meistens in die lokale Bereitschaftswarteschlange ihres idealen Prozessors gestellt, was die Konsistenz der Algorithmen für die Auswahl des logischen Prozessors garantiert. Heterogene Threadplanung (big.LITTLE) Der Kernel geht davon aus, dass ein SMP-System vorliegt, wie wir es eben beschrieben haben. ARM-Prozessoren können jedoch ungleiche Kerne enthalten. Eine typische ARM-CPU (z. B. von Qualcomm) enthält einige leistungsfähige Kerne, die immer nur für kurze Zeit ausgeführt werden und mehr Strom verbrauchen, sowie einige schwächere, die länger laufen und dabei weniger Strom ziehen. Diese Konstruktionsweise wird auch als big.LITTLE bezeichnet. 322 Kapitel 4 Threads
Seit Windows 10 ist es möglich, zwischen diesen Kernen zu unterscheiden und die Threads aufgrund der Größe der Kerne und ihrer Richtlinien z. B. im Hinblick auf den Vordergrundstatus des Threads, seine Priorität und die erwartete Laufzeit zu verteilen. Bei der Initialisierung des Energie-Managers und beim Hinzufügen von Prozessoren im laufenden Betrieb initialisiert Windows den Prozessorsatz durch einen Aufruf von PopInitializeHeteroProcessors. Diese Funktion kann ein Heterosystem simulieren (etwa zu Testzwecken), indem sie wie folgt Registrierungsschlüssel unter HKLM\System\CurrentControlSet\Control\SessionManager\Kernel\ KGroups hinzufügt: QQ Im Namen des Schlüssels wird die Prozessorgruppennummer mithilfe einer zweistelligen Dezimalzahl angegeben. (Denken Sie daran, dass eine Gruppe höchstens 64 Prozessoren enthalten kann.) Beispielsweise steht 00 für die erste Gruppe, 01 für die zweite usw. Auf den meisten Systemen sollte eine Gruppe ausreichen. QQ Jeder Schlüssel enthält den DWORD-Wert SmallProcessorMask. Dies ist die Maske für die Prozessoren, die als »klein« betrachtet werden. Lautet der Wert beispielsweise 3 (die ersten beiden Bits sind gesetzt) und enthält die Gruppe sechs Prozessoren, sind die Prozessoren 0 und 1 klein, die anderen vier dagegen groß. Dies ist im Grunde genommen das Gleiche wie eine Affinitätsmaske. Der Kernel verfügt auch über einige Richtlinienoptionen, die sich für Heterosysteme anpassen lassen. Gespeichert sind sie in globalen Variablen. Tabelle 4–5 nennt einige dieser Variablen und ihre Bedeutung. Variablenname Bedeutung Standardwert KiHeteroSystem Ist das System heterogen? False PopHeteroSystem Heterotyp des Systems: None (0) Simulated (1) EfficiencyClass (2) FavoredCore (3) None (0) PpmHeteroPolicy Threadplanungsrichtlinie: None (0) Manual (1) SmallOnly (2) LargeOnly (3) Dynamic (4) Dynamic (4) KiDynamicHeteroCpuPolicyMask Bestimmt, was bei der Beurteilung der Wichtigkeit eines Threads berücksichtigt wird. 7 (Vordergrundstatus = 1, Priorität = 2, erwartete Laufzeit = 4) → Threadplanung Kapitel 4 323
Variablenname Bedeutung Standardwert KiDefaultDynamicHeteroCpuPolicy Verhalten der Heterorichtlinie Dynamic (siehe oben): All (0) (alle verfügbaren Prozessoren) Large (1) LargeOrIdle (2) Small (3) SmallOrIdle (4) Dynamic (5) (zieht Priorität und andere Maße zur Entscheidung heran) BiasedSmall (6) (zieht Priorität und andere Maße zur Entscheidung heran, bevorzugt aber kleine Prozessoren) BiasedLarge (7) Small (3) KiDynamicHeteroCpuPolicyImportant Richtlinie für einen als wichtig eingestuften dynamischen Thread (siehe mögliche Werte oben) LargeOrIdle (2) KiDynamicHeteroCpuPolicyImportantShort Richtlinie für einen dynamischen Thread, der als wichtig eingestuft ist, aber nur kurzzeitig läuft Small (3) KiDynamicCpuPolicyExpectedRuntime Der Echtzeitwert, der als »schwer« betrachtet wird 5200 ms KiDynamicHeteroCpuPolicyImportantPriority Die Priorität, über die Threads bei der Auswahl der prioritätsgestützten Richtlinie als wichtig eingestuft werden 8 Tabelle 4–5 Kernelvariablen für Heterosysteme Dynamische Richtlinien (siehe Tabelle 4–5) müssen in einen Wichtigkeitswert auf der Grundlage von KiDynamicHeteroPolicyMask und des Threadstatus übersetzt werden. Dies erledigt die Funktion KiConvertDynamicHeteroPolicy, die nacheinander den Vordergrundstatus des Threads, seine Priorität relativ zu KiDynamicHeteroCpuPolicyImportantPriority und seine erwartete Laufzeit prüft. Wird der Thread als wichtig angesehen (wenn die Laufzeit der entscheidende Faktor ist, kann dies auch ein kurzer Thread sein), so wird für Planungsentscheidungen eine Richtlinie auf der Grundlage der Wichtigkeit herangezogen. (In Tabelle 4–5 wäre das KiDynamicHeteroCpuPolicyImportantShort oder KiDynamicHeteroCpuPolicyImportant.) Gruppengestützte Threadplanung Im vorigen Abschnitt haben Sie die Standardimplementierung der Threadplanung in Windows kennengelernt. Seit ihrer Einführung im ersten Release von Windows NT hat sie sich sowohl im Benutzer- als auch im Serverbereich als zuverlässig erwiesen, wobei die Skalierbarkeit mit jedem neuen Release verbessert wurde. Da bei dieser Vorgehensweise jedoch versucht wird, die Prozessorenressourcen gleichmäßig auf konkurrierende Threads gleicher Priorität aufzuteilen, ist es damit nicht möglich, Anforderungen einer höheren Ebene wie eine gleichmäßige Verteilung der Threads auf Benutzer zu erfüllen oder die Möglichkeit zu berücksichtigen, dass 324 Kapitel 4 Threads
einzelnen Benutzern auf Kosten der anderen mehr CPU-Zeit zur Verfügung gestellt wird. Letzteres kann sich bei Terminalumgebungen als problematisch erweisen, in denen Dutzende von Benutzern um CPU-Zeit konkurrieren. Erfolgt die Planung ausschließlich auf der Ebene der einzelnen Threads, kann ein einziger Thread hoher Priorität eines einzelnen Benutzers die Threads aller anderen Benutzer auf dem Computer aushungern. In Windows 8 und Windows Server 2012 wurde ein gruppengestützter Planungsmechanismus auf der Grundlage von Planungsgruppen (KSCHEDULING_GROUP) eingeführt. Eine solche Gruppe schließt eine Richtlinie, Planungsparameter und je einen Kernelblock zur Planungssteuerung (Kernel Scheduling Control Block, KSCB) pro Prozessor in der Gruppe ein. Der Nachteil ist, dass ein Thread auf die Planungsgruppe zeigt, zu der er gehört. Ist dieser Zeiger NULL, wird der Thread von keiner Planungsgruppe gesteuert. Abb. 4–19 zeigt den Aufbau einer Planungsgruppe. In diesem Beispiel gehören die Threads T1 bis T3 zu der Gruppe, T4 dagegen nicht. Richtlinie Planungsgruppe Zähler Abbildung 4–19 Eine Planungsgruppe Im Zusammenhang mit der gruppengestützten Planung werden Ihnen die folgenden Begriffe begegnen: QQ Generation Dies ist der Zeitraum, über den die CPU-Nutzung verfolgt wird. QQ Kontingent laubt ist. Dies ist der Umfang der CPU-Nutzung, der der Gruppe pro Generation er- QQ Gewicht Die relative Wichtigkeit einer Gruppe. Dies ist ein Wert zwischen 1 und 9, wobei der Standardwert 5 beträgt. QQ Gleichmäßige Planung Hierbei können Leerlaufzyklen auf Threads übertragen werden, die ihr Kontingent schon erfüllt haben, wenn keine Threads ausgeführt werden wollen, die noch einen Teil ihres Kontingents zur Verfügung haben. Die KSCB-Struktur enthält folgende Informationen über die CPU: QQ Zyklusnutzung in der Generation QQ Langfristige durchschnittliche Zyklusnutzung. Dadurch können Spitzen bei der Threadaktivität von einem echten Anstieg unterschieden werden. QQ Steuerungsflags etwa für feste Obergrenzen, bei denen ein Thread auch dann keine zusätzliche CPU-Zeit erhält, wenn sie über das zugewiesene Kontingent hinaus zur Verfügung steht. QQ Bereitschaftswarteschlangen für die Standardprioritäten (also die Prioritäten 0 bis 15, da Echtzeitthreads niemals Teil einer Planungsgruppe sind). Gruppengestützte Threadplanung Kapitel 4 325
Ein wichtiger Parameter einer Planungsgruppe ist der Rang, der als Planungspriorität für die gesamte Gruppe der Threads angesehen werden kann. Dabei kennzeichnet 0 den höchsten Rang. Eine höhere Rangnummer bedeutet, dass die Gruppe mehr CPU-Zeit verbraucht hat, weshalb es weniger wahrscheinlich ist, dass sie weitere CPU-Zeit erhält. Der Rang hat immer Vorrang vor der Priorität. Bei zwei Threads mit unterschiedlichem Rang wird unabhängig von der Priorität derjenige mit der niedrigeren Rangnummer bevorzugt. Bei gleichrangigen Threads dagegen wird die Priorität verglichen. Der Rang wird regelmäßig an die Zyklusnutzung angepasst. Rang 0 ist der höchste, weshalb er jede andere Rangnummer aussticht. Er wird stillschweigend folgenden Arten von Threads zugeteilt: QQ Threads, die nicht zu einer Planungsgruppe gehören (»normale« Threads) QQ Threads, deren Kontingent nicht ausgeschöpft ist QQ Threads mit Echtzeitpriorität (16 bis 31) QQ Threads, die auf der IRQL APC_LEVEL (1) in einem kernelkritischen oder geschützten Bereich ausgeführt werden (siehe Kapitel 8 in Band 2). Bei verschiedenen Planungsentscheidungen (z. B. KiQuantumEnd) wird die Planungsgruppe (falls vorhanden) des aktuellen und des ausführungsbereiten Threads berücksichtigt. Gibt es eine Planungsgruppe, so gewinnt der Thread mit der niedrigsten Rangnummer, wobei bei gleichen Rangnummern nach Priorität entschieden wird und bei gleicher Priorität danach, welcher Thread als erster ankam (Umlaufverfahren am Ende des Quantums). Dynamische gleichmäßige Planung (DFSS) Die dynamische gleichmäßige Planung (Dynamic Fair-Share Scheduling, DFSS) ist ein Mechanismus, um die CPU-Zeit gleichmäßig auf die Sitzungen auf einem Computer zu verteilen. Dadurch wird verhindert, dass eine Sitzung die CPU komplett mit Beschlag belegt, wenn einige ihrer Threads eine relativ hohe Priorität haben und oft laufen. Auf Windows Server-Systemen mit der Rolle Remotedesktop ist dieser Mechanismus standardmäßig aktiviert. Sie können ihn jedoch auch auf jedem anderen Server- und Clientsystem einrichten. Die Implementierung beruht auf der im vorherigen Abschnitt beschriebenen gruppengestützten Planung. Wenn der Registrierungshive SOFTWARE im letzten Teil der Systeminitialisierung von Smss. exe initialisiert wird, löst der Prozess-Manager die endgültige Post-Boot-Initialisierung in P­ sBootPhaseComplete aus, die wiederum PspIsDfssEnabled aufrufen. An dieser Stelle entscheidet das System, welcher der beiden CPU-Kontingentmechanismen verwendet werden soll (der herkömmliche oder DFSS). Um DFSS zu aktivieren, muss der Registrierungswert EnableCpuQuota in beiden Kontingentschlüsseln auf einen von null verschiedenen Wert gesetzt werden. Der erste dieser Schlüssel ist HKLM\SOFTWARE\Policies\Microsoft\Windows\Session Manager\Quota System für die richtliniengestützte Einstellung, der zweite ist HKLM\SYSTEM\CurrentControlSet\ Control\Session Manager\Quota System unter dem Systemschlüssel. Letzterer legt fest, ob das System DFSS unterstützt (und ist auf Windows Server-Computern mit der Rolle Remotedesktop standardmäßig auf TRUE gesetzt). 326 Kapitel 4 Threads
Ist DFSS aktiviert, wird die globale Variable PsCpuFairShareEnabled auf TRUE gesetzt, wodurch alle Threads Planungsgruppen zugewiesen werden (außer denen in den Prozessen von Sitzung 0). Die DFSS-Konfigurationsparameter werden durch einen Aufruf von PspReadDfssConfigurationValues in den zuvor erwähnten Registrierungsschlüsseln gelesen und in globalen Variablen gespeichert. Das System überwacht diese Schlüssel. Werden sie verändert, ruft der Benachrichtigungscallback erneut PspReadDfssConfigurationValues auf, um die Konfigurationswerte zu aktualisieren. Tabelle 4–6 zeigt die Werte und ihre Bedeutung. Registrierungswert Kernelvariable Bedeutung Standard­ wert DfssShortTermSharingMS PsDfssShortTermSharingMS Die erforderliche Zeit für die Erhöhung des Gruppenrangs in einem Generationszyklus 30 ms DfssLongTermSharingMS PsDfssLongTermSharingMS Die erforderliche Zeit, um von Rang 0 zu einem von null verschiedenen Rang zu kommen, wenn die Threads in einem Generationszyklus ihr Kontingent überschreiten 15 ms DfssGenerationLengthMS PsDfssGenerationLengthMS Die Dauer der Generation, in der die CPU-Nutzung beobachtet wird 600 ms DfssLongTermFraction1024 PsDfssLongTermFraction1024 Der Wert, der in der Formel für einen exponentiellen gleitenden Durchschnitt zur Berechnung langfristiger Zyklen verwendet wird 512 Tabelle 4–6 DFSS-Konfigurationsparameter in der Registrierung Wird bei aktivierter DFSS eine neue Sitzung erstellt (abgesehen von Sitzung 0), verknüpft MiSessionObjectCreate mit dieser Sitzung eine Planungsgruppe mit dem Standardgewicht 5, also dem Mittel zwischen dem Mindestwert von 1 und dem Höchstwert von 9. Die Planungsgruppe verwaltet je nach Einstellung im Element Type ihrer Richtlinienstruktur (KSCHEDULING_ GROUP_­POLICY) entweder DFSS-Informationen (WeightBased=0) oder Informationen zur CPU-­ Ratensteuerung (RateControl=1; siehe den nächsten Abschnitt). MiSessionObjectCreate ruft KeInsertSchedulingGroup auf, um die Planungsgruppe in die globale Systemliste einzufügen, die in der globalen Variablen KiSchedulingGroupList geführt wird und zur Neuberechnung der Gewichte beim Hinzufügen von Prozessoren im laufenden Betrieb benötigt wird. Die SESSION_ OBJECT-Struktur der betreffenden Sitzung zeigt ebenfalls auf die resultierende Planungsgruppe. Gruppengestützte Threadplanung Kapitel 4 327
Experiment: DFSS in Aktion In diesem Experiment richten Sie ein System zur Verwendung von DFSS ein und beobachten, wie es diesen Mechanismus nutzt. 1. Fügen Sie die in diesem Abschnitt beschriebenen Registrierungsschlüssel und -werte hinzu, um DFSS auf dem System zu aktivieren. (Sie können dieses Experiment auch auf einer VM ausführen.) Starten Sie das System dann neu, damit die Änderungen in Kraft treten. 2. Um sich zu vergewissern, dass DFSS aktiv ist, öffnen Sie eine Kerneldebugsitzung und untersuchen Sie den Wert von PsCpuFairShareEnabled, indem Sie den folgenden Befehl eingeben. Der Wert 1 bedeutet, dass DFSS aktiv ist. lkd> db nt!PsCpuFairShareEnabled L1 fffff800'5183722a 01 3. Schauen Sie sich den aktuellen Thread im Debugger an. (Es sollte einer der Threads sein, die WinDbg ausführen.) Dabei können Sie sehen, dass der Thread zu einer Planungsgruppe gehört und dass sein KSCB nicht NULL ist, da der Thread zum Zeitpunkt der Anzeige ausgeführt wurde: lkd> !thread THREAD ffffd28c07231640 Cid 196c.1a60 Teb: 000000f897f4b000 Win32Thread: ffffd28c0b9b0b40 RUNNING on processor 1 IRP List: ffffd28c06dfac10: (0006,0118) Flags: 00060000 Mdl: 00000000 Not impersonating DeviceMap ffffac0d33668340 Owning Process ffffd28c071fd080 Image: windbg.exe Attached Process N/A Image: N/A Wait Start TickCount 6146 Ticks: 33 (0:00:00:00.515) Context Switch Count 877 IdealProcessor: 0 UserTime 00:00:00.468 KernelTime 00:00:00.156 Win32 Start Address 0x00007ff6ac53bc60 Stack Init ffffbf81ae85fc90 Current ffffbf81ae85f980 Base ffffbf81ae860000 Limit ffffbf81ae85a000 Call 0000000000000000 Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5 Scheduling Group: ffffd28c089e7a40 KSCB: ffffd28c089e7c68 rank 0 4. Geben Sie den Befehl dt ein, um sich die Planungsgruppe anzusehen: lkd> dt nt!_kscheduling_group ffffd28c089e7a40 +0x000 Policy : _KSCHEDULING_GROUP_POLICY +0x008 RelativeWeight : 0x80 → 328 Kapitel 4 Threads
+0x00c ChildMinRate : 0x2710 +0x010 ChildMinWeight : 0 +0x014 ChildTotalWeight : 0 +0x018 QueryHistoryTimeStamp : 0xfed6177 +0x020 NotificationCycles : 0n0 +0x028 MaxQuotaLimitCycles : 0n0 +0x030 MaxQuotaCyclesRemaining : 0n-73125382369 +0x038 SchedulingGroupList : _LIST_ENTRY [ 0xfffff800'5179b110 - 0xffffd28c'081b7078 ] +0x038 Sibling : _LIST_ENTRY [ 0xfffff800'5179b110 0xffffd28c'081b7078 ] +0x048 NotificationDpc : 0x0002eaa8'0000008e _KDPC +0x050 ChildList : _LIST_ENTRY [ 0xffffd28c'062a7ab8 0xffffd28c'05c0bab8 ] +0x060 Parent : (null) +0x080 PerProcessor : [1] _KSCB 5. Erstellen Sie einen weiteren lokalen Benutzer auf dem Computer. 6. Führen Sie CPU Stress in der laufenden Sitzung aus. 7. Lassen Sie einige Threads mit maximaler Aktivität laufen, aber ohne den Computer damit zu überlasten. Im folgenden Beispiel verwenden wir zwei Threads mit maximaler Aktivität auf einer VM mit drei Prozessoren: 8. Drücken Sie (Strg) + (Alt) + (Entf) und wählen Sie Benutzer wechseln. Wählen Sie das Konto für den in Schritt 5 erstellten Benutzer aus und melden Sie sich an. 9. Führen Sie erneut CPU Stress aus und lassen Sie dabei die gleiche Anzahl von Threads mit maximaler Aktivität laufen. 10. Öffnen Sie für den Prozess CPUSTRES das Menü Process, wählen Sie Priority Class und dann High, um seine Prioritätsklasse zu ändern. Ohne DFSS würde dieser Prozess höherer Priorität die CPU fast komplett mit Beschlag belegen, da es vier Threads gibt, die um drei Prozessoren konkurrieren. Einer dieser Threads muss dabei verlieren, und zwar derjenige mit der niedrigen Priorität. 11. Öffnen Sie Process Explorer, doppelklicken Sie auf beide CPUSTRES-Prozesse und rufen Sie die Registerkarte Performance Graph auf. → Gruppengestützte Threadplanung Kapitel 4 329
12. Ordnen Sie die Fenster nebeneinander an. Wie Sie sehen, wird die CPU ungefähr gleichmäßig von den Prozessen genutzt, obwohl sie unterschiedliche Prioritäten aufweisen: 13. Deaktivieren Sie DFSS, indem Sie die Registrierungsschlüssel entfernen. Starten Sie das System neu. 14. Führen Sie das Experiment erneut durch. Jetzt sollten Sie erkennen, dass der Prozess mit höherer Priorität erheblich mehr CPU-Zeit erhält. Grenzwerte für die CPU-Rate DFSS platziert neue Threads automatisch in der Planungsgruppe der Sitzung. Das ist für Terminaldienste zwar sehr gut geeignet, reicht aber als allgemeiner Mechanismus zur Begrenzung der CPU-Zeit von Threads oder Prozessen nicht aus. Durch die Verwendung eines Jobobjekts lässt sich die Infrastruktur der Planungsgruppen auch auf eine feinere Weise nutzen. Wie Sie in Kapitel 3 gelernt haben, kann ein Job einen oder mehrere Prozesse verwalten. Eine der Einschränkungen, die Sie einem Job auferlegen können, ist die CPU-Ratensteuerung. Dazu rufen Sie SetInformationJobObject mit JobObjectCpuRateControlInformation als Informationsklasse und einer Struktur vom Typ JOBOBJECT_CPU_RATE_ CONTROL_INFORMATION mit den eigentlichen Steuerdaten auf. Die Struktur enthält eine Reihe von Flags, mit denen Sie drei verschiedene Einstellungen zur Begrenzung der CPU-Zeit einrichten können: QQ CPU-Rate Dieser Wert kann zwischen 1 und 10.000 liegen und stellt den mit 100 multiplizierten prozentualen Anteil dar. (Beispielsweise müssen Sie für 40 % den Wert 4000 angeben.) QQ Gewichtsgestützte Begrenzung Dies ist ein Wert zwischen 1 und 9 relativ zum Gewicht anderer Jobs. (DFSS ist mit dieser Einstellung vorkonfiguriert.) QQ Minimale und maximale CPU-Rate Diese Werte werden ähnlich angegeben wie bei der ersten Option. Wenn die Threads in einem Job den maximalen Prozentsatz für das Messintervall erreichen (standardmäßig 600 ms), können sie vor Beginn des nächsten Intervalls keine weitere CPU-Zeit mehr erhalten. Mit einem Steuerungsflag können Sie angeben, ob dies ein harter Grenzwert sein soll, sodass er auch dann gilt, wenn freie CPU-Zeit zur Verfügung steht. 330 Kapitel 4 Threads
Diese Grenzwerte führen unter dem Strich dazu, dass alle Threads sämtlicher Prozesse in dem Job in eine neue Planungsgruppe gestellt werden und die Gruppe wie angegeben konfiguriert wird. Experiment: Grenzwert für die CPU-Rate In diesem Experiment richten Sie mithilfe eines Jobobjekts einen Grenzwert für die CPU-Rate ein. Da der Debugger zurzeit noch einen Bug aufweist, ist es am besten, dieses Experiment in einer VM durchzuführen und eine Verbindung zu deren Kernel herzustellen. 1. Starten Sie CPU Stress auf der Test-VM und richten Sie einige Threads so ein, dass sie etwa 50 % der CPU-Zeit verbrauchen. Beispielsweise müssen Sie dazu auf einem Acht-Prozessor-System vier Threads mit maximaler Aktivität laufen lassen: 2. Öffnen Sie Process Explorer, öffnen Sie die Eigenschaften von CPUSTRES und rufen Sie die Registerkarte Performance Graph auf. Die CPU-Nutzung sollte etwa 50 % betragen. 3. Laden Sie das Programm CpuLimit aus den Begleitmaterialien zu diesem Buch herunter. Mit diesem einfachen Werkzeug können Sie die CPU-Nutzung eines einzelnen Prozesses durch Angabe eines harten Grenzwerts beschränken. 4. Führen Sie den folgenden Befehl aus, um die CPU-Nutzung durch den Prozess CPUSTRES auf 20 % zu begrenzen. (Ersetzen Sie dabei 6324 durch die Prozess-ID auf Ihrem System.) Das Werkzeug erstellt einen Job (CreateJobObject), fügt ihm den Prozess hinzu (AssignProcessTokenJobObject) und ruft SetInformationJobObject mit der Informa­ tionsklasse für die CPU-Rate und dem Wert 2000 (20 %) auf. CpuLimit.exe 6324 20 5. In Process Explorer können Sie jetzt einen Abfall auf ca. 20 % erkennen: 6. Öffnen Sie WinDbg auf dem Hostsystem. → Gruppengestützte Threadplanung Kapitel 4 331
7. Stellen Sie eine Verbindung zum Kernel des Testsystems her und klinken Sie sich darin ein. 8. Geben Sie den folgenden Befehl ein, um den Prozess CPUSTRES zu finden: 0: kd> !process 0 0 cpustres.exe PROCESS ffff9e0629528080 SessionId: 1 Cid: 18b4 Peb: 009e4000 ParentCid: 1c4c DirBase: 230803000 ObjectTable: ffffd78d1af6c540 HandleCount: <Data Not Accessible> Image: CPUSTRES.exe 9. Geben Sie den folgenden Befehl ein, um grundlegende Informationen über den Prozess einzusehen: 0: kd> !process ffff9e0629528080 1 PROCESS ffff9e0629528080 SessionId: 1 Cid: 18b4 Peb: 009e4000 ParentCid: 1c4c DirBase: 230803000 ObjectTable: ffffd78d1af6c540 HandleCount: <Data Not Accessible> Image: CPUSTRES.exe VadRoot ffff9e0626582010 Vads 88 Clone 0 Private 450. Modified 4. Locked 0. DeviceMap ffffd78cd8941640 Token ffffd78cfe3db050 ElapsedTime 00:08:38.438 UserTime 00:00:00.000 KernelTime 00:00:00.000 QuotaPoolUsage[PagedPool] 209912 QuotaPoolUsage[NonPagedPool] 11880 Working Set Sizes (now,min,max) (3296, 50, 345) (13184KB, 200KB, 1380KB) PeakWorkingSetSize 3325 VirtualSize 108 Mb PeakVirtualSize 128 Mb PageFaultCount 3670 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 568 Job ffff9e06286539a0 → 332 Kapitel 4 Threads
10. Wie Sie sehen, gibt es ein Jobobjekt, das nicht NULL ist. Lassen Sie sich mit dem Befehl !job seine Eigenschaften anzeigen: 0: kd> !job ffff9e06286539a0 Job at ffff9e06286539a0 Basic Accounting Information TotalUserTime: 0x0 TotalKernelTime: 0x0 TotalCycleTime: 0x0 ThisPeriodTotalUserTime: 0x0 ThisPeriodTotalKernelTime: 0x0 TotalPageFaultCount: 0x0 TotalProcesses: 0x1 ActiveProcesses: 0x1 FreezeCount: 0 BackgroundCount: 0 TotalTerminatedProcesses: 0x0 PeakJobMemoryUsed: 0x248 PeakProcessMemoryUsed: 0x248 Job Flags [close done] [cpu rate control] Limit Information (LimitFlags: 0x0) Limit Information (EffectiveLimitFlags: 0x800) CPU Rate Control Rate = 20.00% Hard Resource Cap Scheduling Group: ffff9e0628d7c1c0 11. Führen Sie CpuLimit für denselben Prozess aus und setzen Sie die CPU-Rate erneut auf 20 %. Daraufhin fällt der CPU-Verbrauch von CPUSTRES auf ca. 4 %. Das liegt daran, dass der neue Job und der ihm zugewiesene Prozess in dem ersten Job verschachtelt werden. Dadurch wird jetzt ein Grenzwert von 20 % verlangt, also 4 %. Dynamisches Hinzufügen und Ersetzen von Prozessoren Wie Sie in den bisherigen Ausführungen gesehen haben, können Entwickler genau festlegen, welche Threads auf welchen Prozessoren laufen dürfen (und im Falle von idealen Prozessoren, welche darauf laufen sollen). Auf Systemen, bei denen die Anzahl der Prozessoren zur Laufzeit unverändert bleibt, funktioniert das sehr gut, beispielsweise auf Desktopcomputern, bei denen es für jegliche Änderungen an der Anzahl der Prozessoren erforderlich ist, den Rechner herunterzufahren. Die Betreiber moderner Serversysteme können sich jedoch keine Ausfallzeiten leisten, um eine CPU auszutauschen oder hinzuzufügen. Es kann sogar sein, dass CPUs gerade dann hinzugefügt werden müssen, wenn der Computer so stark belastet ist, dass er nicht mehr Gruppengestützte Threadplanung Kapitel 4 333
die übliche Leistung bringt. Den Server gerade während einer solchen Spitzenauslastung herunterzufahren, wäre jedoch widersinnig. Daher ist es bei den neuesten Generationen von Servermotherboards und -systemen möglich, Prozessoren im laufenden Betrieb hinzuzufügen und auszutauschen. Das ACPI-BIOS und die zugehörige Hardware auf solchen Computern wurden entsprechend konstruiert, aber damit es funktioniert, muss auch das Betriebssystem mitspielen. Unterstützung für eine dynamische Prozessorauslegung wird durch die HAL bereitgestellt, die den Kernel mithilfe der Funktion KeStartDynamicProcessor über einen neuen Prozessor auf dem System informiert. Diese Routine führt ähnliche Arbeiten aus wie diejenigen, die anfallen, wenn das System beim Start mehr als einen Prozessor erkennt und die mit ihnen verknüpften Strukturen initialisieren muss. Beim dynamischen Hinzufügen eines Prozessors erledigen mehrere Systemkomponenten noch zusätzliche Aufgaben. Beispielsweise weist der Speicher-Manager neue Seiten und Speicherstrukturen zu, die für die CPU optimiert sind, und initialisiert einen neuen DPC-Kernelstack, während der Kernel die globale Deskriptortabelle (GDT), die Interruptdispatchtabelle (IDT), die Prozessorsteuerungsregion (PCR), den Prozessorsteuerungsblock (PRCB) und ähnliche Strukturen für den Prozessor initialisiert. Es werden auch andere Exekutivelemente des Kernels aufgerufen, hauptsächlich um die Look-Aside-Listen für den hinzugefügten Prozessor zu initialisieren. Unter anderem nutzen der E/A-Manager, der Exekutivcode für Look-Aside-Listen, der Cache-Manager und der Objekt-Manager solche Listen für ihre häufig zugewiesenen Strukturen. Schließlich initialisiert der Kernel die DPC-Threadunterstützung für den Prozessor und passt die exportierten Kernelvariablen an, sodass sie den neuen Prozessor angeben. Es werden auch einige Masken im Speicher-Manager und Prozessseeds aktualisiert, die auf der Anzahl der Prozessoren beruhen. Einzelne Merkmale des neuen Prozessors müssen ebenfalls an den Rest des Systems angepasst werden, beispielsweise indem auf ihm die Unterstützung für die Virtualisierung aktiviert wird. Zum Abschluss der Initialisierung wird die WHEA-Komponente (Windows Hardware Error Architecture) darüber informiert, dass ein neuer Prozessor online ist. Auch die HAL ist an diesem Vorgang beteiligt. Sie wird einmal aufgerufen, um den dynamisch hinzugefügten Prozessor zu starten, wenn der Kernel über ihn Bescheid weiß, und ein weiteres Mal, wenn der Kernel die Initialisierung dieses Prozessors abgeschlossen hat. Allerdings sorgen diese Benachrichtigungen und Callbacks nur dafür, dass der Kernel über Prozessoränderungen informiert ist und darauf reagiert. Ein zusätzlicher Prozessor erhöht zwar den Durchsatz des Kernels, hilft aber nicht den Treibern. Für den Umgang mit den Treibern verwendet das System das Standard-Callbackobjekt ProcessorAdd, mit dem sich Treiber für Benachrichtigungen registrieren können. Ähnlich wie die Callbacks, die Treiber über den Energiestatus oder über Änderungen der Systemzeit informieren, ermöglicht dieser Callback dem Treibercode, z. B. einen neuen Arbeitsthread zu erstellen, wenn das gewünscht ist, um mehr Arbeit zur selben Zeit zu erledigen. Nachdem die Treiber benachrichtigt sind, wird als letzte Kernelkomponente der Plug-andPlay-Manager aufgerufen, der den Prozessor zum Geräteknoten des Systems hinzufügt und die Interrupts neu verteilt, damit der neue Prozessor die bereits für andere Prozessoren registrierten Interrupts handhaben kann. CPU-hungrige Anwendungen können den neuen Prozessor ebenfalls nutzen. 334 Kapitel 4 Threads
Eine plötzliche Änderung der Affinität kann sich auf eine laufende Anwendung jedoch störend auswirken, insbesondere beim Übergang von einem Einzel- zu einem Mehrprozessorsystem, denn dadurch können Race Conditions auftreten oder die Arbeit falsch verteilt werden (da der Prozess möglicherweise beim Start die idealen Verhältnisse auf der Grundlage der ihm bekannten Anzahl von Prozessoren berechnet hat). Daher nutzen Anwendungen die Vorteile eines dynamisch hinzugefügten Prozessors standardmäßig nicht, sondern müssen ihn anfordern. Mit den Windows-APIs SetProcessAffinityUpdateMode und QueryProcessAffinityUpdateMode, die den nicht dokumentierten Systemaufruf NtSet/QueryInformationProcess nutzen, teilen Anwendungen dem Prozess-Manager mit, ob ihre Affinität aktualisiert werden soll (durch das Flag AffinityUpdateEnable in der EPROCESS-Struktur) oder nicht (durch das Flag AffinityPermanent). Diese Einstellung wird einmalig vorgenommen. Wenn eine Anwendung dem System mitgeteilt hat, dass ihre Affinität dauerhaft sein soll, kann sie nicht später ihre Meinung ändern und Affinitätsaktualisierungen anfordern. Im Rahmen von KeStartDynamicProcessor wurde nach der Neuverteilung der Interrupts ein neuer Schritt hinzufügt, nämlich ein Aufruf des Prozess-Managers, um über PsUpdate​ ActiveProcessAffinity Affinitätsaktualisierungen durchzuführen. Bei einigen Kernprozessen und -diensten von Windows ist die Möglichkeit für Affinitätsaktualisierungen bereits aktiviert, wohingegen Drittanbietersoftware neu kompiliert werden muss, um diesen neuen API-Aufruf nutzen zu können. Der Systemprozess, die Svchost-Prozesse und Smss sind mit der dynamischen Ergänzung von Prozessoren kompatibel. Arbeitsfactories (Threadpools) Arbeitsfactories sind der interne Mechanismus zur Implementierung der Threadpools des Benutzermodus. Früher waren die Threadpoolroutinen komplett im Benutzermodus in der Bibliothek Ntdll.dll implementiert. Die Windows-API enthielt außerdem verschiedene Funktionen für Timer mit Wartemöglichkeiten (CreateTimerQueue, CreateTimerQueueTimer usw.), Wartecallbacks (RegisterWaitForSingleObject) und die Verarbeitung von Arbeitselementen mit automatischer Threaderstellung und -löschung in Abhängigkeit vom Umfang der zu erledigenden Arbeit (QueueUserWorkItem). Ein Problem bei der alten Implementierung bestand darin, dass in einem Prozess nur ein Threadpool erstellt werden konnte, wodurch sich manche Szenarien schwer umsetzen ließen. So war es beispielsweise nicht direkt möglich, Prioritäten für Arbeitselemente durch den Aufbau von zwei Threadpools für unterschiedliche Anforderungen aufzustellen. Das andere Problem betraf die Tatsache, dass sich die Implementierung im Benutzermodus (in Ntdll.dll) befand. Da der Kernel die direkte Kontrolle über Planung, Erstellung und Beendigung von Threads ohne die Kosten übernehmen kann, die für diese Operationen im Benutzermodus gewöhnlich anfallen, befindet sich der Großteil der Funktionalität zur Unterstützung von Benutzermodus-Threadpools jetzt im Kernel. Das vereinfacht auch den Code, den Entwickler schreiben müssen. So lässt sich ein Arbeitspool in einem Remoteprozess jetzt mit einem einzigen API-Aufruf erstellen statt mit der komplizierten Folge von Aufrufen im virtuellen Arbeitsspeicher, die sonst erforderlich Arbeitsfactories (Threadpools) Kapitel 4 335
waren. Bei dem neuen Modell stellt Ntdll.dll nur noch die Schnittstellen und die APIs höherer Ebene bereit, die für den Umfang mit dem Kernelcode der Arbeitsfactories erforderlich sind. Bereitgestellt wird diese Threadpool-Funktionalität des Kernels von dem Objekt-Manager-Typ TpWorkerFactory, vier nativen Systemaufrufen für die Verwaltung der Factory und ihrer Arbeitsthreads (NtCreateWorkerFactory, NtWorkerFactoryWorkerReady, NtReleaseWorker­ FactoryWorker und NtShutdownWorkerFactory), zwei nativen Aufrufen zum Abfragen und Festlegen (NtQueryInformationWorkerFactory und NtSetInformationWorkerFactory) und einem Warteaufruf (NtWaitForWorkViaWorkerFactory). Wie andere native Systemaufrufe stellen auch diese im Benutzermodus ein Handle für das TpWorkerFactory-Objekt bereit, das Informationen wie den Namen und die Objektattribute, die gewünschte Zugriffsmaske und einen Sicherheitsdeskriptor enthält. Anders als bei anderen in die Windows-API eingepackten Systemaufrufen erfolgt die Verwaltung der Threadpools jedoch durch den nativen Code von Ntdll.dll. Entwickler müssen also mit undurchsichtigen Deskriptoren arbeiten: einem TP_POOL-Zeiger für einen Threadpool und anderen undurchsichtigen Zeigern für Objekte, die aus einem Pool erstellt wurden, u. a. TP_WORK (Arbeitscallback), TP_TIMER (Timercallback) und TP_WAIT (Wartecallback). Diese Strukturen enthalten verschiedene Informationen, darunter das Handle auf das TpWorkFactory-Objekt. Die Implementierung der Arbeitsfactory ist dafür zuständig, die Arbeitsthreads zuzuweisen, den Eintrittspunkt eines Arbeitsthreads im Benutzermodus aufzurufen und einen Zähler für Mindest- und Höchstanzahl von Threads zu führen (um permanente komplett dynamische Arbeitspools zu ermöglichen) sowie andere Buchführungsinformationen zu verfolgen. Dadurch können Operationen wie das Herunterladen des Threadpools mit einem einzigen Kernelaufruf erledigt werden, da der Kernel als einzige Komponente für das Erstellen und Beenden der Threads zuständig ist. Die Tatsache, dass der Kernel neue Threads auf der Grundlage der Mindest- und Höchstzahlen nach Bedarf dynamisch erstellt, verbessert die Skalierbarkeit von Anwendungen, die diese neue Implementierung von Threadpools nutzen. Eine Arbeitsfactory erstellt einen neuen Thread, wenn sämtliche der folgenden Bedingungen erfüllt sind: QQ Die dynamische Threaderstellung ist aktiviert. QQ Die Anzahl der verfügbaren Arbeitsthreads ist geringer als die für die Factory eingestellte Höchstzahl von Arbeitsthreads (wobei der Standardwert 500 beträgt). QQ Die Arbeitsfactory verfügt über gebundene Objekte (z. B. einen ALPC-Port, auf den der Arbeitsthread wartet), oder ein Thread wurde aktiviert und in den Pool gestellt. QQ Mit einem Arbeitsthread sind ausstehende E/A-Anforderungspakete verknüpft (IRPS; siehe Kapitel 6). Des Weiteren beendet die Factory Threads, wenn sie länger als zehn Sekunden (Standardeinstellung) unbeschäftigt sind, also keine Arbeitselemente verarbeitet haben. Auch bei der alten Implementierung konnten Entwickler stets so viele Threads wie möglich verwenden (was durch die Anzahl der Prozessoren auf dem System bestimmt wurde), doch jetzt können Anwendungen, die Threadpools nutzen, automatisch die Vorteile neuer Prozessoren nutzen, die zur Laufzeit hinzugefügt werden. Das ist dank der im vorherigen Abschnitt erläuterten Unterstützung für dynamisch hinzugefügte Prozessoren möglich. 336 Kapitel 4 Threads
Erstellen von Arbeitsfactories Die Einrichtung von Arbeitsfactories ist lediglich ein Wrapper für den Umgang mit alltäglichen Aufgaben, die ansonsten auf Kosten der Leistung im Benutzermodus ausgeführt werden müssten. Ein Großteil des neuen Threadpoolcodes verbleibt in Ntdll.dll. (Bei der Nutzung nicht dokumentierter Funktionen ließe sich auf der Grundlage der Arbeitsfactories auch eine andere Threadpoolimplementierung aufbauen.) Es ist auch nicht der Code für Arbeitsfactories, der für Skalierbarkeit, die internen Wartemechanismen und eine effiziente Verarbeitung sorgt, sondern eine viel ältere Windows-Komponente, nämlich die E/A-Abschlussports oder genauer die Kernelwarteschlangen (KQUEUE). Um eine Arbeitsfactory zu erstellen, muss bereits ein E/A-Abschlussport im Benutzermodus angelegt und das Handle übergeben werden. Dieser E/A-Abschlussport sorgt dafür, dass die Benutzermodusimplementierung auf Arbeit wartet – allerdings nicht durch die APIs für den Port, sondern durch Systemaufrufe der Arbeitsfactory. Bei dem »Freigabe«-Aufruf der Arbeitsfactory (der Arbeit in eine Warteschlange stellt) handelt es sich intern jedoch um einen Wrapper um IoSetIoCompletionEx, die die ausstehende Arbeit erweitert, und bei dem »Warte«-Aufruf um einen Wrapper um IoRemoveIoCompletion. Beide Routinen rufen die Implementierung der Kernelwarteschlange auf. Die Aufgabe des Arbeitsfactorycodes besteht also darin, einen dauerhaften statischen oder einen dynamischen Threadpool zu verwalten; das E/A-Abschlussmodell in die Schnittstellen einzupacken, die automatisch dynamische Threads erstellen, um einen Stillstand der Arbeitswarteschlangen zu verhindern; und die globalen Aufräum- und Beendigungsoperationen beim Herunterfahren einer Factory zu vereinfachen sowie neue Anforderungen an die Factory in einer solchen Situation zu blockieren. Die Exekutivfunktion, die die Arbeitsfactory erstellt, ist NtCreateWorkerFactory. Sie nimmt mehrere Argumente zur Gestaltung des Threadpools entgegen, z. B. die maximale Anzahl zu erstellender Threads und die ursprüngliche Größe des zugesicherten und des reservierten Stacks. Die Windows-API CreateThreadpool verwendet dagegen die Standardstackgrößen, die in das Abbild der ausführbaren Datei eingebettet sind – ebenso wie die Standardfunktion ­CreateThread. Allerdings ist es bei der Windows-API nicht möglich, diese Standardwerte zu überschreiben. Das ist kein sehr glücklicher Umstand, denn oft sind für Threads aus Threadpools keine tiefen Aufrufstacks erforderlich, weshalb es von Vorteil wäre, wenn man kleine Stacks zuweisen könnte. Die von der Implementierung der Arbeitsfactory verwendeten Datenstrukturen befinden sich nicht unter den öffentlichen Symbolen, doch wie Sie im nächsten Experiment sehen werden, ist es trotzdem möglich, sich einige der Arbeitspools anzusehen. Außerdem gibt die API NtQueryInformationWorkerFactory fast sämtliche Felder der Arbeitsfactorystruktur aus. Arbeitsfactories (Threadpools) Kapitel 4 337
Experiment: Threadpools einsehen Aufgrund seiner Vorteile wird der Threadpoolmechanismus von vielen Systemkomponenten und Anwendungen genutzt, vor allem für den Umgang mit Ressourcen wie ALPC-Ports (um eingehende Anforderungen auf passende und skalierbare Weise dynamisch zu verarbeiten). Eine der Möglichkeiten, um herauszufinden, welche Prozesse eine Arbeitsfactory nutzen, besteht darin, sich die Handleliste in Process Explorer anzusehen. Gehen Sie dazu wie folgt vor: 1. Starten Sie Process Explorer. 2. Öffnen Sie das Menü View und wählen Sie Show Unnamed Handles and Mappings. Da Arbeitsfactories von Ntdll.dll leider nicht benannt werden, ist dieser Schritt erforderlich, um die Handles sehen zu können. 3. Markieren Sie in der Prozessliste eine Instanz von Svchost.exe. 4. Öffnen Sie das Menü View und wählen Sie Show Lower Pane, um den unteren Bereich der Handletabelle einzublenden. 5. Öffnen Sie das Menü View, wählen Sie Lower Pane View und dann Handles, um die Tabelle im Handlemodus anzuzeigen. 6. Rechtsklicken Sie auf die Spaltenköpfe im unteren Bereich und wählen Sie Select Columns. 7. Sorgen Sie dafür, dass die Spalten Type und Handle Value mit einem Häkchen versehen sind. 8. Klicken Sie auf den Spaltenkopf Type, um die Tabelle nach dem Typ zu sortieren. 9. Scrollen Sie nach unten, bis Sie ein Handle vom Typ TpWorkerFactory finden. 10. Klicken Sie auf den Spaltenkopf Handle, um die Tabelle nach dem Handlewert zu sortieren. Dadurch erhalten Sie die Anzeige aus dem folgenden Screenshot. Beachten Sie, dass dem TpWorkerFactory-Handle unmittelbar ein IoCompletion-Handle vorausgeht. Wie bereits erwähnt, muss vor dem Erstellen einer Arbeitsfactory ein Handle für einen E/A-Abschlussport angelegt werden, zu dem die Arbeit gesendet wird. → 338 Kapitel 4 Threads
11. Doppelklicken Sie in der Prozessliste auf den ausgewählten Prozess, rufen Sie die Registerkarte Threads auf und klicken Sie auf die Spalte Start Address. Dadurch erhalten Sie die Anzeige aus dem folgenden Screenshot. Arbeitsfactorythreads lassen sich leicht an ihrem Ntdll.dll-Eintrittspunkt TppWorkerThread erkennen (wobei Tpp für Thread Pool Private steht). Wenn Sie sich andere Arbeitsthreads ansehen, werden Sie feststellen, dass einige davon auf Objekte wie Ereignisse warten. Ein Prozess kann über mehrere Threadpools verfügen, wobei jeder davon Threads für völlig unterschiedliche Aufgaben haben kann. Der Entwickler ist dafür zuständig, die Arbeit zuzuweisen und die Threadpool-APIs aufzurufen, um diese Arbeit über Ntdll.dll zu registrieren. Zusammenfassung In diesem Kapitel ging es um die Struktur von Threads. Wir haben uns angesehen, wie sie erstellt und verwaltet werden und wie Windows entscheidet, welche Threads wie lange auf welchen Prozessoren ausgeführt werden sollen. Im nächsten Kapitel werfen wir einen genaueren Blick auf einen der wichtigsten Aspekte eines jeden Betriebssystems, nämlich die Speicherverwaltung. Zusammenfassung Kapitel 4 339

K apite l 5 Speicherverwaltung In diesem Kapitel lernen Sie, wie Windows den virtuellen Arbeitsspeicher implementiert und den Teil davon verwaltet, der sich im physischen Arbeitsspeicher befindet. Auch die internen Datenstrukturen, Komponenten und Algorithmen des Speicher-Managers werden beschrieben. Bevor wir diese Mechanismen untersuchen, sehen wir uns jedoch noch einmal die grundlegenden Dienste an, die der Speicher-Manager zur Verfügung stellt, sowie wichtige Begriffe wie reservierter, zugesicherter und gemeinsam genutzter Speicher. Einführung in den Speicher-Manager Die virtuelle Größe eines Prozesses beträgt auf 32-Bit-Windows standardmäßig 2 GB. Wenn das Abbild so gekennzeichnet ist, dass es mit großen Adressräumen umgehen kann, und das System mit einer besonderen Option gestartet wird (die weiter hinten in diesem Kapitel im Abschnitt »x86-Adressraumlayouts« erklärt wird), kann ein 32-Bit-Prozess auf 32-Bit-Windows auf 3 GB und auf 64-Bit-Windows auf 4 GB anwachsen. Auf den 64-Bit-Versionen von Windows 8 und Windows Server 2012 beträgt die Größe des virtuellen Prozessadressraums 8192 GB (8 TB) und auf den 64-Bit-Versionen von Windows 8.1 und Windows Server 2012 R2 (und höher) 128 TB. Wie Sie in Kapitel 2 und dort insbesondere in Tabelle 2–2 gesehen haben, unterstützt Windows je nach Version und Edition physischen Arbeitsspeicher im Umfang von 2 GB bis 24 TB. Da der virtuelle Adressraum größer oder kleiner sein kann als der physische Arbeitsspeicher des Computers, hat der Speicher-Manager die beiden folgenden Hauptaufgaben: QQ Er muss den virtuellen Adressraum eines Prozesses auf den physischen Arbeitsspeicher abbilden (oder ihm zuordnen), damit die richtigen physischen Adressen referenziert werden, wenn ein Thread, der im Kontext dieses Prozesses läuft, in dem virtuellen Adressraum liest oder schreibt. Der Teil des virtuellen Adressraums eines Prozesses, der sich im physischen Arbeitsspeicher befindet, wird Arbeitssatz genannt. Mehr darüber erfahren Sie im Abschnitt »Arbeitssätze« weiter hinten in diesem Kapitel. QQ Er muss einen Teil der Inhalte des Arbeitsspeichers auf die Festplatte auslagern, wenn Threads versuchen, mehr physischen Arbeitsspeicher zu verwenden, als verfügbar ist, und diese Inhalte bei Bedarf wieder in den physischen Arbeitsspeicher zurückholen. Der Speicher-Manager kümmert sich jedoch nicht nur um die Verwaltung des virtuellen Arbeitsspeichers, sondern stellt auch grundlegende Dienste bereit, auf denen verschiedene Windows-Umgebungsteilsysteme aufbauen. Dazu gehören im Speicher abgebildete oder zugeordnete Dateien (die intern als Abschnittsobjekte bezeichnet werden), Copy-on-Write-Speicher (Kopieren beim Schreiben) sowie Unterstützung für Anwendungen, die einen umfangreichen, 341
dünn besetzten Adressraum nutzen. Des Weiteren bietet der Speicher-Manager Prozessen eine Möglichkeit, größere Mengen des physischen Speichers zuzuweisen und zu verwenden, als auf einmal in ihrem virtuellen Adressraum zugeordnet werden können, etwa auf 32-Bit-Systemen mit mehr als 3 GB physischem Speicher. Dies wird im Abschnitt »AWE (Address Windowing ­Extensions))« weiter hinten in diesem Kapitel erklärt. Das Systemsteuerungsapplet System bietet die Möglichkeit, Größe, Anzahl und Speicherort von Auslagerungsdateien festzulegen. Die dabei verwendeten Begriffe scheinen darauf hinzudeuten, dass der virtuelle Arbeitsspeicher das Gleiche ist wie die Auslagerungsdatei. Das ist aber nicht der Fall. Die Auslagerungsdatei ist nur ein Aspekt des virtuellen Speichers. Selbst wenn Sie gar keine Auslagerungsdatei verwenden, kann Windows immer noch virtuellen Speicher nutzen. Diese Unterscheidung wird in diesem Kapitel noch ausführlich erklärt. Komponenten des Speicher-Managers Der Speicher-Manager gehört zur Windows-Exekutive und befindet sich daher in der Datei Ntoskrnl.exe. Er bildet die größte Komponente der Exekutive, was schon auf seine Wichtigkeit und Vielschichtigkeit hindeutet. Kein Teil des Speicher-Managers liegt in der HAL. Er besteht aus den folgenden Komponenten: QQ Ein Satz von Exekutiv-Systemdiensten, um virtuellen Speicher zuzuweisen und zu verwalten und die Zuweisung aufzuheben. Die meisten dieser Dienste werden durch die WindowsAPI oder durch die Schnittstellen von Kernelmodus-Gerätetreibern bereitgestellt. QQ Ein Traphandler für ungültige Zuordnungen und Zugriffsfehler, um von der Hardware erkannte Speicherverwaltungsausnahmen aufzulösen und virtuelle Seiten im Namen eines Prozesses speicherresident zu machen. QQ Sechs Routinen oberster Ebene, die jeweils in einem von sechs Kernelmodusthreads im Systemprozess laufen: 342 • Der Balance-Set-Manager (KeBalanceSetManager, Priorität 17) Einmal pro Sekunde und sowie dann, wenn der freie Arbeitsspeicher unter einen festgelegten Schwellenwert fällt, ruft der Balance-Set-Manager den Arbeitssatz-Manager (MwWorkingSetManager) auf, eine innere Routine, die sich um die Richtlinien zur Speicherverwaltung wie Verkleinerung des Arbeitssatzes, Alterung und das Schreiben von modifizierten Seiten kümmert. • Der Prozess-/Stack-Umlagerer (KeSwapProcessOrStack, Priorität 23) Diese Komponente führt die Auslagerung und Einlagerung des Prozess- und des Kernelthread­ stacks durch. Der Balance-Set-Manager und der Threadplanungscode im Kernel wecken diesen Thread auf, wenn eine entsprechende Operation erforderlich ist. • Der Schreibthread für geänderte Seiten (MiModifiedPageWriter, Priorität 18) Er schreibt die geänderten Seiten, die auf der entsprechenden Liste vermerkt sind, wieder in die zugehörigen Auslagerungsdateien. Dieser Thread wird aufgeweckt, wenn die Liste der modifizierten Seiten verkleinert werden muss. Kapitel 5 Speicherverwaltung
• Der Schreibthread für zugeordnete Seiten (MiMappedPageWriter, Priorität 18) Er schreibt geänderte Seiten in zugeordneten Dateien auf die Festplatte oder einen Remotespeicherplatz. Aufgeweckt wird er, wenn die Liste der modifizierten Seiten verkleinert werden muss oder wenn sich Seiten für zugeordnete Dateien schon länger als fünf Minuten auf dieser Liste befinden. Dieser zweite Schreibthread für geänderte Seiten ist notwendig, da er Seitenfehler hervorrufen kann, die zu Anforderungen freier Seiten führen. Gäbe es in einer Situation, in der keine freien Seiten vorhanden sind, nur einen einzigen Schreibthread für geänderte Seiten, so könnte das System beim Warten auf freie Seiten in ein Deadlock eintreten. • Der Segmentdereferenzierungsthread (MiDereferenceSegmentThread, Priorität 19) Dieser Thread ist für die Cachereduzierung und für das Wachstum und die Verkleinerung der Auslagerungsdatei zuständig. Wenn es beispielsweise keinen virtuellen Adressraum für das Wachstum des ausgelagerten Pools gibt, verkleinert dieser Thread den Seitencache, sodass der als sein Anker genutzte ausgelagerte Pool zur Wiederverwendung freigegeben werden kann. • Der Nullseitenthread (MiZeroPageThread, Priorität 0) Dieser Thread setzt die Seiten auf der Liste der freien Seiten auf null, sodass ein Cache aus Nullseiten für spätere Nullforderungsseitenfehler zur Verfügung steht. In einigen Fällen erfolgt diese Nullsetzung durch die schnellere Funktion MiZeroInParallel. Mehr zu diesem Thema erfahren Sie im Abschnitt »Seitenlistendynamik« weiter hinten in diesem Kapitel. Jede dieser Komponenten wird weiter hinten in diesem Kapitel noch ausführlich behandelt. Die einzige Ausnahme bildet der Segmentdereferenzierungsthread, der in Kapitel 14 von Band 2 beschrieben wird. Große und kleine Seiten Die Speicherverwaltung erfolgt in fest umrissenen Teilstücken, die als Seiten bezeichnet werden. Die Hardwareeinheit zur Speicherverwaltung übersetzt die virtuellen Adressen mit der Genauigkeit von Seiten in physische Adressen. Eine Seite ist daher die kleinste Einheit des Schutzes auf Hardwareebene. (Die Möglichkeiten für den Seitenschutz werden im Abschnitt »Speicherschutz« weiter hinten in diesem Kapitel beschrieben.) Die Prozessoren, auf denen Windows laufen kann, lassen zwei Arten von Seiten zu, kleine und große, wobei die konkrete Größe von der Prozessorarchitektur abhängt (siehe Tabelle 5–1). Architektur Kleine Seite Große Seite Kleine Seiten pro großer Seite x86 (PAE) 4 KB 2 MB 512 x64 4 KB 2 MB 512 ARM 4 KB 4 MB 1024 Tabelle 5–1 Seitengrößen Einführung in den Speicher-Manager Kapitel 5 343
Bei einigen Prozessoren kann die Seitengröße eingestellt werden, allerdings nutzt Windows diese Möglichkeit nicht. Der Hauptvorteil, den große Seiten bieten, ist die Geschwindigkeit der Adressübersetzung für Verweise auf Daten auf solchen Seiten. Beim ersten Verweis auf irgendein Byte auf einer großen Seite werden die Informationen, die auch zur Übersetzung von Verweisen auf jegliche anderen Bytes auf dieser Seite nötig sind, in den Look-Aside-Puffer für die Übersetzung gestellt (Translation Look-Aside Buffer, TLB; siehe den Abschnitt »Adressübersetzung« weiter hinten in diesem Kapitel). Bei kleinen Seiten sind für denselben Bereich virtueller Adressen mehr TLB-Einträge erforderlich, wodurch Einträge häufiger überschrieben werden und neue virtuelle Adressen übersetzt werden müssen. Das wiederum bedeutet, dass bei Verweisen auf virtuelle Adressen außerhalb der kleinen Seite, deren Übersetzung zwischengespeichert wurde, wieder ein Zugriff auf die Seitentabellenstrukturen erfolgen muss. Der TLB ist ein sehr kleiner Cache, der bei der Verwendung großer Seiten besser genutzt wird. Um auf Systemen mit mehr als 2 GB RAM die Vorteile großer Seiten genießen zu können, ordnet Windows sowohl Kernelabbilder (Ntoskrnl.exe und Hal.dll) als auch Kerndaten des Betriebssystems (wie den ursprünglichen Teil des nicht ausgelagerten Pools und die Daten­ strukturen, die den Status der einzelnen physischen Speicherseiten beschreiben) großen Seiten zu. Auch E/A-Adressraumanforderungen (Aufrufe von MmMapIoSpace durch Gerätetreiber) werden automatisch großen Seiten zugeordnet, wenn sie eine ausreichende Länge und eine passende Ausrichtung aufweisen. Des Weiteren erlaubt Windows Anwendungen, ihre eigenen Abbilder, ihren privaten Arbeitsspeicher und auslagerungsgepufferte Abschnitte in großen Seiten zuzuordnen (mithilfe des Flags MEM_LARGE_PAGES in den Funktionen VirtualAlloc, V­ irtualAllocEx und VirtualAllocExNuma). Außerdem können Sie weitere Gerätetreiber angeben, die in großen Seiten zugeordnet werden sollen, indem Sie dem Registrierungsschlüssel HKLM\ SYSTEM\­ CurrentControlSet\Control\Session Manager\Memory Management den Mehrfachstringwert LargePageDrivers hinzufügen und darin die Namen der Treiber als separate, mit NULL angeschlossene Strings angeben. Die Zuordnung großer Seiten kann fehlschlagen, wenn das Betriebssystem schon längere Zeit läuft, da der physische Speicher für jede dieser Seiten eine hohe Anzahl physisch benachbarter kleiner Seiten umfassen muss (siehe Tabelle 5–1). Des Weiteren müssen physische Seiten an bestimmten Grenzen beginnen. Beispielsweise können auf einem x64-System die physischen Seiten 0 bis 511 und 512 bis 1023 für große Seiten verwendet werden, die Seiten 10 bis 521 aber nicht. Wenn das System in Betrieb ist, wird der freie physische Speicher jedoch fragmentiert. Das ist bei der Zuweisung kleiner Seiten kein Problem, kann aber dazu führen, dass die Zuweisung großer Seiten fehlschlägt. Große Seiten können auch nicht ausgelagert werden. Zur Zuordnung in großen Seiten muss der Aufrufer daher über das Recht SeLockMemoryPrivilege verfügen. Außerdem wird der auf diese Weise zugeordnete Arbeitsspeicher nicht als Bestandteil des Prozessarbeitssatzes angesehen (siehe den Abschnitt »Arbeitssätze« weiter hinten in diesem Kapitel) und unterliegt auch nicht den Jobeinschränkungen für die Verwendung des virtuellen Speichers. Auf x64-Systemen mit Windows 10 Version 1607 und Windows Server 2016 können bei der Zuordnung von großen Seiten auch riesige Seiten von 1 GB verwendet werden. Das geschieht 344 Kapitel 5 Speicherverwaltung
automatisch, wenn die angeforderte Zuweisung mehr als 1 GB beträgt, aber sie muss kein Mehrfaches davon betragen. Um beispielsweise 1040 MB zuzuweisen, werden eine Riesenseite (1024 MB) und acht große Seiten (8 x 2 MB = 16 MB) verwendet. Große Seiten haben jedoch eine unangenehme Nebenwirkung. Jede Seite (ob riesig, groß oder klein) muss mit einem Schutz zugeordnet werden, der für die gesamte Seite gilt, da der Hardwarespeicherschutz seitenweise erfolgt. Wenn eine große Seite sowohl schreibgeschützten Code als auch les- und schreibbare Daten enthält, muss die Seite als les- und schreibbar gekennzeichnet werden, wodurch auch der Code schreibbar wird. In einem solchen Fall können Gerätetreiber und Kernelmoduscode – sei es aus böser Absicht oder aufgrund eines Bugs – den Betriebssystem- oder Treibercode, der eigentlich schreibgeschützt sein soll, ohne Zugriffsverletzung überschreiben. Werden dagegen kleine Seiten verwendet, um den Kernelmoduscode des Betriebssystems zuzuordnen, dann können die schreibgeschützten Teile von Ntoskrnl.exe und Hal.dll als schreibgeschützte Seiten zugeordnet werden. Die Verwendung kleiner Seiten verringert zwar die Effizienz der Adressübersetzung, doch wenn ein Gerätetreiber oder anderer Kernelmoduscode versucht, den schreibgeschützten Teil des Betriebssystems zu ändern, stürzt das System sofort ab, wobei die Ausnahmeinformationen auf die dafür verantwortliche Anweisung im Treiber hinweisen. Wäre der Schreibvorgang dagegen zulässig, würde das System später auf eine schwerer zu diagnostizierende Weise abstürzen, nachdem eine andere Komponente versucht hat, die beschädigten Daten zu verwenden. Wenn Sie befürchten, dass der Kernelcode beschädigt werden könnte, aktivieren Sie den Treiberverifizierer (siehe Kapitel 6), der die Verwendung großer Seiten unterbindet. In diesem und den folgenden Kapiteln bezeichnen wir mit Seite immer eine kleine Seite, sofern nicht ausdrücklich etwas anderes angegeben oder durch den Zusammenhang deutlich gemacht wird. Die Speichernutzung untersuchen Die Leistungsobjekte Arbeitsspeicher und Prozess bieten Zugriff auf die meisten Informationen zur Speichernutzung durch das System und die Prozesse. In diesem Kapitel verweisen wir häufig auf Leistungsindikatoren zu den jeweils beschriebenen Komponenten und geben Beispiele und Anleitungen für eigene Experimente. Beachten Sie aber, dass die verschiedenen Werkzeuge bei der Anzeige von Informationen über den Arbeitsspeicher oft unterschiedliche und manchmal uneinheitliche oder irreführende Bezeichnungen verwenden. Das folgende Experiment macht dies deutlich. (Die in diesem Beispiel erwähnten Begriffe werden übrigens in nachfolgenden Abschnitten erläutert.) Einführung in den Speicher-Manager Kapitel 5 345
Hinweis: Informationen über den Arbeitsspeicher einsehen Die Registerkarte Leistung des Task-Managers, die Sie in dem folgenden Screenshot auf einem System mit Windows 10 Version 1607 sehen, zeigt grundlegende Informationen über den physischen und den virtuellen Arbeitsspeicher an. Dies ist allerdings nur einen Teil der Informationen, die über Leistungsindikatoren zur Verfügung stehen. Die Bedeutung der Werte wird in der anschließenden Tabelle erklärt. Wert Bedeutung Speicherauslastung Die Höhe der Linie in diesem Diagramm zeigt die Menge des von Windows verwendeten physischen Arbeitsspeichers an (ein Maß, für das es keinen Leistungsindikator gibt). Der Bereich über der Linie entspricht dem Wert Verfügbar im unteren Bereich. Die Gesamthöhe des Diagramms steht für den Gesamtwert, der rechts darüber angezeigt wird (in diesem Beispiel 31,9 GB). Dies ist der gesamte RAM, den das Betriebssystem verwenden kann. BIOS-Schattenseiten, Gerätearbeitsspeicher usw. sind darin nicht enthalten. Speicherzusammensetzung Dieses Diagramm zeigt die Beziehung zwischen dem aktiv verwendeten, dem Standby-, dem modifizierten und dem freien und Nullseiten-Arbeitsspeicher an. (All diese Begriffe werden im Verlauf dieses Kapitels erklärt.) Gesamter physischer Arbeitsspeicher (rechts über dem Diagramm) Gibt die Menge des von Windows nutzbaren physischen Arbeitsspeichers an. → 346 Kapitel 5 Speicherverwaltung
Wert Bedeutung In Verwendung (komprimiert) Dies ist die Menge des zurzeit verwendeten physischen Arbeitsspeichers mit dem Betrag an komprimiertem physischen Speicher in Klammern. Wenn Sie den Mauszeiger über diesen Wert halten, wird angezeigt, wie viel Arbeitsspeicher durch die Komprimierung gespart wird (siehe den Abschnitt »Speicherkomprimierung« weiter hinten in diesem Kapitel). Im Cache Dieser Wert stellt die Summe der folgenden Leistungsindikatoren aus der Kategorie Arbeitsspeicher dar: Cachebytes, Geänderte Seitenliste – Bytes, Standbycache-Kernbytes, Standbycache-Bytes mit normaler Priorität, Standbycache-Reservebytes. Verfügbar Dies ist die Menge an Speicher, die unmittelbar zur Verwendung durch das Betriebssystem, für Prozesse und Treiber zur Verfügung steht. Der Wert entspricht der kombinierten Größe des Standby-, des freien und des Nullseitenspeichers. Frei Dieser Wert zeigt den freien und den Nullseitenspeicher in Bytes an. Um ihn sichtbar zu machen, müssen Sie den Mauszeiger über das rechte Ende des Diagramms mit der Speicherzusammensetzung halten (vorausgesetzt, dass Sie noch genügend freien Arbeitsspeicher für einen solchen Vorgang zur Verfügung haben). Commit ausgeführt (»committed«, was in anderen Windows-Programmen auch als »zugesicherter Speicher« wiedergegeben wird) Die beiden hier gezeigten Zahlen entsprechen den Werten der Leistungsindikatoren Zugesicherte Bytes und Zusagegrenze. Ausgelagerter Pool Dieser Wert gibt die Gesamtgröße des ausgelagerten Pools an, wobei sowohl die freien als auch die zugewiesenen Bereiche berücksichtigt werden. Nicht ausgelagerter Pool Dieser Wert gibt die Gesamtgröße des nicht ausgelagerten Pools an, wobei sowohl die freien als auch die zugewiesenen Bereiche berücksichtigt werden. Um sich die Nutzung des ausgelagerten und des nicht ausgelagerten Pools im Einzelnen anzusehen, verwenden Sie das Programm Poolmon, das weiter hinten in diesem Kapitel im Abschnitt »Die Poolnutzung überwachen« beschrieben wird. Der Process Explorer von Sysinternals zeigt erheblich mehr Daten über den physischen und den virtuellen Speicher an. Klicken Sie auf dem Hauptbildschirm dieses Programms auf View, wählen Sie System Information und rufen Sie die Registerkarte Memory auf. Das folgende Beispiel stammt von einem 64-Bit-System mit Windows 10. Die meisten der hier gezeigten Indikatoren werden wir in den entsprechenden Abschnitten weiter hinten in diesem Kapitel erklären. → Einführung in den Speicher-Manager Kapitel 5 347
Um erweiterte Informationen über den Arbeitsspeicher zu gewinnen, stehen Ihnen auch noch zwei weitere Sysinternal-Tools zur Verfügung: QQ VMMap QQ RAMMap Zeigt die Nutzung des virtuellen Speichers in einem Prozess detailliert an. Zeigt die Nutzung des physischen Speichers ausführlich an. Diese Werkzeuge werden in Experimenten weiter hinten in diesem Kapitel vorgestellt. Auch mit dem Kerneldebuggerbefehl !vm können Sie sich grundlegende Informationen über die Speicherverwaltung ansehen. Dieser Befehl ist vor allem dann praktisch, wenn Sie sich ein Absturzabbild oder ein hängendes System ansehen. Das folgende Beispiel zeigt die Ausgabe auf einem 64-Bit-System mit Windows 10 und 32 GB RAM: lkd> !vm Page File: \??\C:\pagefile.sys Current: 1048576 Kb Free Space: 1034696 Kb Minimum: 1048576 Kb Maximum: 4194304 Kb Page File: \??\C:\swapfile.sys Current: 16384 Kb Free Space: Minimum: 16384 Kb Maximum: 16376 Kb 24908388 Kb No Name for Paging File Current: 58622948 Kb Free Space: 57828340 Kb Minimum: 58622948 Kb Maximum: 58622948 Kb Physical Memory: 8364281 ( 33457124 Kb) Available Pages: 4627325 ( 18509300 Kb) ResAvail Pages: 7215930 ( 28863720 Kb) → 348 Kapitel 5 Speicherverwaltung
Locked IO Pages: Free System PTEs: 0 ( 0 Kb) 4295013448 (17180053792 Kb) Modified Pages: 68167 ( 272668 Kb) Modified PF Pages: 68158 ( 272632 Kb) 0 ( 0 Kb) Modified No Write Pages: NonPagedPool Usage: NonPagedPoolNx Usage: NonPagedPool Max: 495 ( 1980 Kb) 269858 ( 1079432 Kb) 4294967296 (17179869184 Kb) PagedPool 0 Usage: 371703 ( 1486812 Kb) PagedPool 1 Usage: 99970 ( 399880 Kb) PagedPool 2 Usage: 100021 ( 400084 Kb) PagedPool 3 Usage: 99916 ( 399664 Kb) PagedPool 4 Usage: 99983 ( 399932 Kb) 771593 ( 3086372 Kb) PagedPool Usage: PagedPool Maximum: 4160749568 (16642998272 Kb) Session Commit: 12210 ( 48840 Kb) Shared Commit: 344197 ( 1376788 Kb) Special Pool: 0 ( 0 Kb) Shared Process: 19244 ( 76976 Kb) Pages For MDLs: 419675 ( 1678700 Kb) 0 ( 0 Kb) NonPagedPool Commit: 270387 ( 1081548 Kb) PagedPool Commit: 771593 ( 3086372 Kb) Pages For AWE: Driver Commit: Boot Commit: System PageTables: 24984 ( 99936 Kb) 100044 ( 400176 Kb) 5948 ( 23792 Kb) 18202 ( 72808 Kb) 299 ( 1196 Kb) 33 ( 132 Kb) Sum System Commit: 1986816 ( 7947264 Kb) Total Private: 2126069 ( 8504276 Kb) VAD/PageTable Bitmaps: ProcessLockedFilePages: Pagefile Hash Pages: Misc/Transient Commit: 18422 ( 73688 Kb) Committed pages: 4131307 ( 16525228 Kb) Commit limit: 9675001 ( 38700004 Kb) ... Die Werte außerhalb der Klammern betreffen kleine Seiten (4 KB). Viele der Angaben in dieser Ausgabe werden wir in diesem Kapitel noch genauer erklären. Einführung in den Speicher-Manager Kapitel 5 349
Interne Synchronisierung Wie die anderen Komponenten der Windows-Exekutive ist auch der Speicher-Manager vollständig eintrittsinvariant und ermöglicht die gleichzeitige Ausführung auf Mehrprozessorsystemen. Das heißt, zwei Threads können Ressourcen auf solche Weise erwerben, dass sie nicht gegenseitig die Daten des anderen beschädigen. Dazu setzt der Speicher-Manager verschiedene interne Synchronisierungsmechanismen ein, z. B. Spinlocks und ineinandergreifende Anweisungen, um den Zugriff auf seine eigenen internen Datenstrukturen zu steuern. (Synchronisierungsobjekte werden in Kapitel 8 von Band 2 beschrieben.) Unter anderem muss der Speicher-Manager den Zugriff auf die folgenden systemweiten Ressourcen synchronisieren: QQ Dynamisch zugewiesene Teile des virtuellen Systemadressraums QQ Systemarbeitssätze QQ Kernelspeicherpools QQ Liste der geladenen Treiber QQ Liste der Auslagerungsdateien QQ Listen des physischen Speichers QQ Abbildgestützte Strukturen für die Randomisierung des Adressraumlayouts (Address Space Layout Randomization, ASLR) QQ Die Einträge in der Datenbank der Seitenframenummern (Page Frame Number, PFN) Folgende prozesseigenen Datenstrukturen zur Speicherverwaltung bedürfen der Synchronisierung: QQ Arbeitssatzsperre Diese Sperre wird eingerichtet, wenn Änderungen an den Arbeitssätzen vorgenommen werden. QQ Adressraumsperre Diese Sperre wird bei Änderungen am Adressraum eingerichtet. Für beide Sperren werden Pushlocks verwendet. Eine Beschreibung finden Sie in Kapitel 8 von Band 2. Vom Speicher-Manager bereitgestellte Dienste Der Speicher-Manager stellt eine Reihe von Systemdiensten bereit, um virtuellen Arbeitsspeicher zuzuweisen und freizugeben, Arbeitsspeicher von mehreren Prozessen gemeinsam nutzen zu lassen, Dateien im Arbeitsspeicher abzubilden, virtuelle Seiten auf die Festplatte zu entleeren, Informationen über einen Bereich virtueller Seiten abzurufen, den Schutz virtueller Seiten zu ändern und virtuelle Seiten im Speicher zu sperren. Wie bei anderen Diensten der Windows-Exekutive kann der Aufrufer auch bei den Speicherverwaltungsdiensten ein Prozesshandle übergeben, um den Prozess anzugeben, dessen virtueller Speicher bearbeitet werden soll. Dadurch kann der Aufrufer seinen eigenen Arbeitsspeicher oder – sofern er über die entsprechenden Berechtigungen verfügt – den eines anderen Prozesses ändern. Beispielsweise hat ein Prozess standardmäßig das Recht, den virtuellen Speicher seiner Kindprozesse zu bearbeiten. Er kann im Namen des Kindprozesses Arbeitsspei- 350 Kapitel 5 Speicherverwaltung
cher zuweisen, darin lesen und schreiben und die Zuweisung aufheben, indem er die Dienste für den virtuellen Speicher aufruft und dabei ein Handle für den Kindprozess als Argument übergibt. Diese Möglichkeit wird von Teilsystemen verwendet, um den Arbeitsspeicher ihrer Clientprozesse zu verwalten. Sie ist auch für Debugger unverzichtbar, da diese Programme in der Lage sein müssen, im Arbeitsspeicher des zu debuggenden Prozesses zu lesen und zu schreiben. Die meisten dieser Dienste werden durch die Windows-API bereitgestellt. Wie Sie in Abb. 5–1 sehen, enthält diese API vier Gruppen von Funktionen für die Speicherverwaltung in Anwendungen: QQ API für virtuellen Speicher Dies ist die maschinennächste API für allgemeine Speicherzuweisungen und Aufhebungen der Zuweisung. Sie bearbeitet grundsätzlich einzelne Seiten. Gleichzeitig ist sie die leistungsfähigste API, da sie sämtliche Möglichkeiten des Speicher-Managers zur Verfügung stellt. Zu ihren Funktionen gehören VirtualAlloc, V­ irtualFree, VirtualProtect, VirtualLock usw. QQ Heap-API Diese API bietet Funktionen für kleine Zuweisungen (gewöhnlich weniger als eine Seite). Sie greift intern auf die API für virtuellen Speicher zurück, setzt aber eine Verwaltungsschicht darauf. Zu den Heap-Manager-Funktionen gehören HeapAlloc, HeapFree, HeapCreate, HeapReAlloc usw. Der Heap-Manager wird im gleichnamigen Abschnitt weiter hinten in diesem Kapitel behandelt. QQ Lokale/globale APIs Dies sind Überbleibsel aus den 16-Bit-Versionen von Windows, die jetzt mithilfe der Heap-API implementiert sind. QQ API für im Speicher zugeordnete Dateien Diese Funktionen ermöglichen die Zuordnung von Dateien im Speicher und die gemeinsame Nutzung des Speichers durch kooperierende Prozesse. Dazu gehören CreateFileMapping, OpenFileMapping, MapViewOfFile usw. Lokale/globale APIs C/C++-Laufzeit Heap-API API für virtuellen Speicher Dateizuordnungs-API Benutzermodus Kernelmodus Speicher-Manager Abbildung 5–1 Gruppen von Speicher-APIs im Benutzermodus Der gestrichelte Kasten steht für eine typische C/C++-Laufzeitimplementierung der Speicherverwaltung (Funktionen wie malloc, free, realloc und C++-Operatoren wie new und delete), die auf die Heap-API zurückgreift. Durch die gestrichelte Darstellung wird angedeutet, dass diese Implementierung compilerabhängig und nicht obligatorisch ist (allerdings ist sie häufig Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 351
anzutreffen). Die in Ntdll.dll implementierten Gegenstücke für die C-Laufzeitumgebung verwenden die Heap-API. Der Speicher-Manager stellt auch verschiedene Dienste für andere Kernelmoduskomponenten in der Exekutive und für Gerätetreiber zur Verfügung. Dazu gehören die Zuweisung von physischem Speicher und die Aufhebung dieser Zuweisung sowie das Sperren von Seiten im physischen Speicher für den direkten Speicherzugriff (Direct Memory Access, DMA). Die Namen dieser Funktionen beginnen mit dem Präfix Mm. Darüber hinaus werden noch einige Exekutiv-Unterstützungsroutinen verwendet, die streng genommen kein Teil des Speicher-Managers sind. Ihre Namen beginnen mit Ex, und sie dienen zur Zuweisung von System-Heaps (ausgelagerter und nicht ausgelagerter Pool), zur Aufhebung dieser Zuweisung und zur Bearbeitung von Look-Aside-Listen. Damit werden wir uns weiter hinten in diesem Kapitel im Abschnitt »Kernelmodusheaps (Systemspeicherpools)« noch eingehend beschäftigen. Seitenstatus und Speicherzuweisungen Die Seiten im virtuellen Adressraum eines Prozesses sind entweder frei, reserviert, zugesichert oder gemeinsam nutzbar. Wird auf zugesicherte und gemeinsam nutzbare Seiten zugegriffen, so werden sie letzten Endes in gültige Seiten im physischen Speicher übersetzt. Zugesicherte Seiten werden auch als private Seiten bezeichnet, da sie nicht mit anderen Prozessen geteilt werden können, was bei gemeinsam nutzbaren Seiten möglich ist. Private Seiten werden mithilfe der Windows-Funktionen VirtualAlloc, VirtualAllocEx und VirtualAllocExNuma zugewiesen. Dabei wird letzten Endes die Funktion NtAllocateVirtualMemory im Speicher-Manager ausgeführt. Diese Funktionen können Speicher sowohl zusichern als auch reservieren. Beim Reservieren wird ein zusammenhängender Bereich von virtuellen Adressen für die zukünftige Nutzung beiseite gestellt (z. B. für ein Array), wodurch nur vernachlässigbare Systemressourcen verbraucht werden. Im Verlauf der Anwendung werden dann nach Bedarf Teile dieses reservierten Adressraums zugesichert. Wenn die Größenanforderungen im Voraus bekannt sind, kann ein Prozess die Reservierung und Zusicherung auch im selben Funktionsaufruf durchführen. In jedem Fall sind die resultierenden zugesicherten Seiten für jeden Thread in dem Prozess zugänglich. Der Versuch, auf freien oder reservierten Speicher zuzugreifen, führt dagegen zu einer Ausnahme aufgrund einer Zugriffsverletzung, da die Seite keinem Speicherbereich zugeordnet ist, zu dem der Verweis aufgelöst werden kann. Zugesicherte (private) Seiten werden als Nullforderungsseiten erstellt, wenn zum ersten Mal auf sie zugegriffen wird. Wenn es die Nachfrage nach physischem Speicher erfordert, kann das Betriebssystem sie später automatisch in die Auslagerungsdatei schreiben. »Privat« bedeutet hier, dass diese Seiten für andere Prozesse normalerweise unzugänglich sind. Einige Funktionen, z. B. ReadProcessMemory und WriteProcessMemory, scheinen einen prozessübergreifenden Speicherzugriff zuzulassen, allerdings führen sie Kernelmoduscode im Kontext des Zielprozesses aus. Außerdem ist es erforderlich, dass der Sicherheitsdeskriptor des Zielprozesses dem zugreifenden Prozess das Recht PROCESS_VM_READ bzw. PROCESS_VM_WRITE gewährt oder dass der zugreifende Prozess das Recht SeDebugPrivilege hat, das standardmäßig nur Mitgliedern der Adminis­ tratorengruppe gewährt wird. 352 Kapitel 5 Speicherverwaltung
Gemeinsam genutzte Seiten werden gewöhnlich auf eine Sicht des Abschnitts abgebildet. Dies wiederum ist ein Teil einer Datei oder eine ganze Datei, kann aber auch ein Teil der Auslagerungsdatei sein. Alle gemeinsam genutzten Seiten können mit anderen Prozessen geteilt werden. Abschnitte werden in der Windows-API als Dateizuordnungsobjekte verfügbar gemacht. Beim ersten Zugriff irgendeines Prozesses auf eine gemeinsam genutzte Seite wird sie in der zugehörigen Datei gelesen, es sei denn, dass sie mit der Auslagerungsdatei verknüpft ist. In letzterem Fall wird sie als Nullforderungsseite erstellt. Während sich die Seite im physischen Arbeitsspeicher befindet, können weitere Prozesse, die auf sie zugreifen, einfach die Seiteninhalte verwenden, die bereits im Arbeitsspeicher sind. Gemeinsam genutzte Seiten können auch vom System vorab abgerufen werden. In zwei späteren Abschnitten dieses Kapitels – »Gemeinsam genutzter Arbeitsspeicher und zugeordnete Dateien« und »Abschnittsobjekte« – werden gemeinsam genutzte Seiten ausführlicher erläutert. Ein Mechanismus, der als Schreiben von geänderten Seiten bezeichnet wird, schreibt Seiten auf die Festplatte oder in einen Remotespeicher, wenn sie aus dem Arbeitssatz des Prozesses in die systemweite Liste der geänderten Seiten verschoben werden. (In den deutschen Versionen von Windows wird sie unglücklicherweise als »geänderte Seitenliste« bezeichnet. Diese Liste sowie Arbeitssätze werden weiter hinten in diesem Kapitel erklärt.) Die Seiten im Speicher zugeordneter Dateien können auch mit einem ausdrücklichen Aufruf von FlushViewOfFile oder mit dem Schreibthread für zugeordnete Seiten wieder zurück in die ursprünglichen Dateien auf der Festplatte geschrieben werden, wenn der Speicherbedarf dies erfordert. Mit den Funktionen VirtualFree und VirtualFreeEx können Sie die Zusicherung privater Seiten aufheben bzw. den Adressraum freigeben. Der Unterschied besteht darin, dass Speicher mit aufgehobener Zusicherung immer noch reserviert ist, wohingegen freigegebener Speicher weder zugesichert noch reserviert ist. Der zweistufige Vorgang, bei dem virtueller Arbeitsspeicher erst reserviert und dann zugesichert wird, verzögert die Zusicherung von Seiten (und damit auch die Anrechnung auf den gesamten zugesicherten Speicher des Systems, was im nächsten Abschnitt beschrieben wird), bis sie notwendig wird, bewahrt aber den Zusammenhang des virtuellen Adressraums. Die Reservierung von Speicher ist eine relativ billige Operation, da dabei sehr wenig Arbeitsspeicher verbraucht wird. Es müssen lediglich einige vergleichsweise kleine interne Datenstrukturen für den Status des Prozessadressraums aktualisiert oder erstellt werden. Diese Datenstrukturen – die Seitentabellen und VADs (Virtual Address Descriptors) – werden weiter hinten in diesem Kapitel erklärt. Ein sehr häufig anzutreffendes Beispiel für die Reservierung eines umfangreichen Adressraums und die bedarfsweise Zusicherung kleiner Teile daraus bildet der Benutzermodusstack für einen Thread. Wird ein Thread erstellt, so wird sein Stack dadurch angelegt, dass ein zusammenhängender Teil des Prozessadressraums reserviert wird. (Die Standardgröße beträgt 1 MB, aber diesen Wert können Sie mit den Funktionen CreateThread und CreateRemoteThread(Ex) überschreiben oder mit dem Linkerflag /STACK für das gesamte ausführbare Abbild ändern.) Standardmäßig wird die erste Seite des Stacks zugesichert und die nächste Seite als Wächterseite gekennzeichnet (aber nicht zugesichert), um Verweise abzufangen, die über den zugesicherten Teil des Stacks hinausgehen, und diesen zu erweitern. Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 353
Experiment: Der Unterschied zwischen reservierten und zugesicherten Seiten Mit dem Sysinternals-Programm TestLimit können Sie große Menge an reserviertem und an privatem, zugesichertem virtuellen Arbeitsspeicher zuweisen. Die Unterschiede zwischen diesen Vorgängen können Sie dann in Process Explorer beobachten. Gehen Sie dazu wie folgt vor: 1. Öffnen Sie zwei Eingabeaufforderungen. 2. Rufen Sie an einer der Eingabeaufforderungen TestLimit auf, um eine große Menge an Speicher zu reservieren: C:\temp>testlimit -r 1 -c 800 Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - wwww.sysinternals.com Process ID: 18468 Reserving private bytes 1 MB at a time ... Leaked 800 MB of reserved memory (800 MB total leaked). Lasterror: 0 The operation completed successfully. 3. Sichern Sie an der anderen Eingabeaufforderung die gleiche Menge an Arbeitsspeicher zu: C:\temp>testlimit -m 1 -c 800 Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - wwww.sysinternals.com Process ID: 14528 Leaking private bytes 1 KB at a time ... Leaked 800 MB of private memory (800 MB total leaked). Lasterror: 0 The operation completed successfully. 4. Starten Sie den Task-Manager, rufen Sie die Registerkarte Details auf und fügen Sie die Spalte Zugesicherte Größe hinzu. → 354 Kapitel 5 Speicherverwaltung
5. Suchen Sie die beiden Vorkommen von TestLimit.exe in der Liste. Sie sehen etwa wie folgt aus: Der Task-Manager zeigt zwar die Größe des zugesicherten Speichers an, verfügt aber über keine Indikatoren für den reservierten Speicher. 6. Starten Sie Process Explorer. 7. Rufen Sie die Registerkarte Process Memory auf und aktivieren Sie die Spalten Private Bytes und VirtualSize. 8. Machen Sie die beiden TestLimit.exe-Prozesse im Hauptfenster ausfindig: → Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 355
Die virtuellen Größen der beiden Prozesse sind gleich, aber nur bei einem von ihnen hat Private Bytes einen ähnlichen Wert wie Virtual Size. Der riesige Unterschied bei dem anderen TestLimit-Prozess (Prozess-ID 18468) kommt durch den reservierten Speicher zustande. Diesen Vergleich können Sie auch in der Leistungsüberwachung durchführen, indem Sie sich die Indikatoren Virtuelle Bytes und Private Bytes in der Kategorie Prozess ansehen. Gesamter zugesicherter Speicher und Zusicherungsgrenzwert Auf der Seite Arbeitsspeicher der Registerkarte Leistung im Task-Manager finden Sie den Abschnitt Commit ausgeführt. (In der deutschen Version von Windows wird der Begriff »committed« leider sehr uneinheitlich übersetzt. Neben der hier verwendeten Bezeichnung »Commit ausgeführt« werden Sie auch »zugesagt«, »festgelegt« und vor allem »zugesichert« finden.) Darunter stehen zwei Zahlen, von denen die erste den gesamten zugesicherten virtuellen Speicher auf dem System angibt (in Windows nicht sehr aussagekräftig einfach »festgelegter virtueller Speicher« genannt). Es gibt einen systemweiten Grenzwert (»Zusagegrenze«) für die Menge des zugesicherten virtuellen Speichers. Er entspricht der aktuellen Gesamtgröße aller Auslagerungsdateien zuzüglich der Menge an RAM, die das Betriebssystem nutzen kann. Dies ist die zweite der beiden Zahlen unter Commit ausgeführt. Der Speicher-Manager kann diesen Grenzwert automatisch erhöhen, indem er eine oder mehrere Auslagerungsdateien erweitert, die noch nicht ihre festgelegte Höchstgröße erreicht haben. Der gesamte zugesicherte Speicher und der systemweite Zusicherungsgrenzwert werden im Abschnitt »Gesamter zugesicherter Speicher und systemweiter Zusicherungsgrenzwert« weiter hinten in diesem Kapitel noch genauer erklärt. Seiten im Arbeitsspeicher festhalten Im Allgemeinen ist es besser, den Speicher-Manager entscheiden zu lassen, welche Seiten im physischen Speicher verbleiben sollen. Unter besonderen Umständen kann es für eine Anwendung oder einen Gerätetreiber jedoch notwendig sein, Seiten im physischen Speicher festzuhalten. Das kann auf zwei Weisen geschehen: QQ Windows-Anwendungen können die Funktion VirtualLock aufrufen, um Seiten im Arbeitssatz ihres Prozesses zu verriegeln. Die Seiten verbleiben dann im Speicher, bis sie ausdrücklich entriegelt werden oder der Prozess, der sie verriegelt hat, beendet wird. Die Anzahl der Seiten, die ein Prozess verriegeln kann, ist auf die Mindestgröße seines Arbeitssatzes abzüglich acht Seiten beschränkt. Muss er mehr Seiten im Speicher festhalten, kann er die Mindestgröße seines Arbeitssatzes mit der Funktion SetProcessWorkingSetSizeEx erhöhen (siehe den Abschnitt »Verwaltung von Arbeitssätzen« weiter hinten in diesem Kapitel). QQ Gerätetreiber können die Kernelmodusfunktionen MmProbeAndLockPages, MmLockPagableCodeSection, MmLockPagableDataSection und MmLockPagableSectionByHandle aufrufen. Mit diesem Mechanismus verriegelte Seiten verbleiben im Arbeitsspeicher, bis sie ausdrücklich 356 Kapitel 5 Speicherverwaltung
entriegelt werden. Bei den letzten drei dieser APIs gibt es keine Einschränkungen für die Anzahl der Seiten, die im Speicher festgehalten werden können, da die Gesamtmenge der residenten, verfügbaren Seiten beim ersten Laden des Treibers bestimmt wird. Dadurch ist sichergestellt, dass es niemals zu einem Systemabsturz aufgrund von übermäßiger Verriegelung kommen kann. Bei der ersten API muss der Grenzwert für die Anzahl verriegelbarer Seiten abgerufen werden, da sie sonst einen Fehlerstatus zurückgibt. Granularität der Zuweisung Windows richtet die Regionen des reservierten Prozessadressraums an den Grenzen aus, die durch die systemweite Zuweisungsgranularität bestimmt sind. Deren Wert können Sie mit den Funktionen WindowsGetSystemInfo und GetNativeSystemInfo abrufen. Er beträgt 64 KB. Bei dieser Granularität kann der Speicher-Manager Metadaten zur Unterstützung verschiedener Prozessoperationen effizient zuweisen (z. B. VADs, Bitmaps usw.). Dieser Wert verringert auch die Gefahr, dass Anwendungen, die Annahmen für die Ausrichtung der Zuweisung treffen, geändert werden müssen, falls eine Unterstützung für zukünftige Prozessoren mit großen Seiten (etwa bis zu 64 KB) oder mit virtuell indizierten Caches hinzukommt, die eine systemweite Ausrichtung des physischen und virtuellen Speichers erfordern würde. Der Kernelmoduscode von Windows unterliegt nicht denselben Einschränkungen, sondern kann Arbeitsspeicher in der Größenordnung einzelner Seiten reservieren (wobei diese Möglichkeit aus den zuvor genannten Gründen nicht für Gerätetreiber verfügbar gemacht wird). Diese Granularität wird hauptsächlich verwendet, um TEB-Zuweisungen dichter packen zu können. Da es sich ausschließlich um einen internen Mechanismus handelt, kann der Code leicht geändert werden, wenn bei einer zukünftigen Plattform andere Werte erforderlich sein sollten. Zur Unterstützung von 16-Bit- und MS-DOS-Anwendungen auf x86-Systemen bietet der Speicher-Manager das Flag MEM_DOS_LIM für die API MapViewOfFileEx an, mit dem eine Granularität einer einzelnen Seite erzwungen werden kann. Wenn eine Region im Adressraum reserviert ist, stellt Windows sicher, dass die Größe und die Basisadresse dieser Region Vielfache der systemweiten Seitengröße sind. Wenn Sie beispielsweise eine Region von 18 KB reservieren wollen, werden daher auf einem x86-System, das 4-KB-Seiten verwendet, in Wirklichkeit 20 KB reserviert. Geben Sie für eine 18-KB-Region eine Basisadresse von 3 KB an, so werden 24 KB reserviert. Der VAD für die Zuweisung wird ebenfalls auf die 64-KB-Begrenzung gerundet, sodass der Rest unzugänglich ist. Gemeinsam genutzter Arbeitsspeicher und zugeordnete Dateien Wie die meisten modernen Betriebssysteme bietet auch Windows einen Mechanismus, um Arbeitsspeicher zwischen Prozessen und dem Betriebssystem zu teilen. Der gemeinsam genutzte Speicher kann als der Arbeitsspeicher definiert werden, der für mehr als einen Prozess sichtbar oder im virtuellen Adressraum mehr als eines Prozesses vorhanden ist. Wenn beispielsweise Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 357
zwei Prozesse dieselbe DLL nutzen, ist es sinnvoll, die referenzierten Codeseiten für diese DLL nur einmal in den physischen Speicher zu laden und diese Seiten unter allen Prozessen zu teilen, die die DLL verwenden (siehe Abb. 5–2). Code in Kernel32.dll Code in Kernel32.dll Code in Kernel32.dll Prozess A RAM Prozess B Code von Prozess B EXE-Code Code von Prozess A EXE-Code Abbildung 5–2 Von zwei Prozessen gemeinsam genutzter Speicher Jeder Prozess hat hier nach wie vor seinen privaten Speicherbereich für seine privaten Daten, doch der DLL-Code und nicht modifizierte Datenseiten können gefahrlos gemeinsam genutzt werden. Wie Sie später noch sehen werden, erfolgt diese Form von gemeinsamer Nutzung automatisch, da Codeseiten in ausführbaren Abbildern – EXE- und DLL-Dateien sowie verschiedenen anderen Typen, z. B. Bildschirmschonern (SCR), bei denen es sich im Grunde genommen um DLLs unter einem anderen Namen handelt – als »nur ausführbar« zugeordnet werden und schreibbare Seiten als »Kopieren beim Schreiben« (Copy-on-Write; siehe den Abschnitt »Kopieren beim Schreiben« weiter hinten in diesem Kapitel). Abb. 5–2 zeigt zwei Prozesse, die sich eine nur einmal im physischen Arbeitsspeicher zugeordnete DLL teilen. Der Code im Abbild (EXE) wird dagegen nicht gemeinsam genutzt, da die beiden Prozesse unterschiedliche Abbilder ausführen. Prozesse, die dasselbe Abbild ausführen – etwa zwei oder mehr Notepad-Prozesse –, teilen sich jedoch auch den EXE-Code. Die Grundelemente, die der Speicher-Manager zur Einrichtung des gemeinsam genutzten Speichers verwendet, werden als Abschnittsobjekte bezeichnet und als Dateizuordnungsobjekte in der Windows-API verfügbar gemacht. Die interne Struktur und Implementierung dieser Objekte wird weiter hinten in diesem Kapitel im Abschnitt »Abschnittsobjekte« erläutert. Mit diesen Grundelementen des Speicher-Managers werden virtuelle Adressen so zugeordnet, als befänden sie sich im Hauptspeicher, unabhängig davon, ob sie sich tatsächlich dort, in der Auslagerungsdatei oder in einer anderen Datei befinden, auf die die Anwendung zugreifen möchte. Ein Abschnitt kann von einem oder mehreren Prozessen geöffnet werden. Abschnittsobjekte stehen also nicht unbedingt für gemeinsam genutzten Speicher. Ein Abschnittsobjekt kann mit einer offenen Datei auf der Festplatte (einer zugeordneten Datei) oder mit zugesichertem Speicher verbunden sein (für gemeinsam genutzten Speicher). In letzterem Fall spricht man von auslagerungsgepufferten Abschnitten (»page-file backed«), da die Seiten nicht in eine zugeordnete Datei, sondern in die Auslagerungsdatei geschrieben werden, wenn der Speicherbedarf dies erfordert. (Da Windows auch ohne Auslagerungsdatei ausgeführt werden kann, ist es möglich, dass diese Abschnitte in Wirklichkeit nur vom physischen Speicher »gepuffert« werden.) Wie andere leere Seiten, die im Benutzermodus sichtbar 358 Kapitel 5 Speicherverwaltung
gemacht werden (z. B. private zugesicherte Seiten), werden auch gemeinsam genutzte zugesicherte Seiten beim ersten Zugriff immer mit Nullen gefüllt, damit keine sensiblen Daten austreten können. Um ein Abschnittsobjekt zu erstellen, rufen Sie die Windows-Funktion CreateFileMapping, CreateFileMappingFromApp oder CreateFileMappingNuma(Ex) auf. Geben Sie ein Handle zu einer zuvor geöffneten Datei an, um den Abschnitt dieser Datei zuzuordnen, oder INVALID_HANDLE_ VALUE, um einen auslagerungsgepufferten Abschnitt anzulegen. Optional können Sie auch einen Namen und einen Sicherheitsdeskriptor angeben. Hat der Abschnitt einen Namen, so können andere Prozesse ihn mit OpenFileMapping und den CreateFileMapping*-Funktionen öffnen. Es ist auch möglich, den Zugriff auf Abschnittsobjekte durch Handlevererbung zu gewähren (indem Sie beim Öffnen oder Erstellen des Handles angeben, dass es vererbbar sein soll) oder durch Handleduplizierung (mit DuplicateHandle). Gerätetreiber können Abschnittsobjekte auch mit den Funktionen ZwOpenSection, ZwMapViewOfSection und ZwUnmapViewOfSection bearbeiten. Ein Abschnittsobjekt kann auf Dateien verweisen, die zu groß für den Adressraum des Prozesses sind. (Bei auslagerungsgepufferten Abschnittsobjekten muss jedoch in der Auslagerungsdatei oder im RAM ausreichend Platz dafür vorhanden sein.) Wenn ein Prozess auf ein solches sehr großes Abschnittsobjekt zugreifen will, darf er nur den Teil zuordnen, den er benötigt (was als eine Sicht des Abschnitts bezeichnet wird), indem er MapViewOfFile(Ex), MapViewOfFileFromApp oder MapViewOfFileExNuma aufruft und den gewünschten Bereich angibt. Durch die Zuordnung einer Sicht spart der Prozess Adressraum. Windows-Anwendungen können zugeordnete Dateien verwenden, um die Datei-E/A zu vereinfachen, da die Dateien dabei im Adressraum der Anwendung im Arbeitsspeicher erscheinen. Abschnittsobjekte werden jedoch nicht nur von Benutzeranwendungen genutzt. Der Abbildlader ordnet damit ausführbare Abbilder, DLLs und Gerätetreiber im Speicher zu, und der Cache-Manager verwendet sie für den Zugriff auf Daten in zwischengespeicherten Dateien. (Weitere Informationen über den Cache-Manager erhalten Sie in Kapitel 14 von Band 2.) Die Implementierung gemeinsam genutzter Speicherabschnitte, insbesondere was die Adressübersetzung und die internen Datenstrukturen angeht, wird im Abschnitt »Abschnittsobjekte« weiter hinten in diesem Kapitel erläutert. Experiment: Im Speicher zugeordnete Dateien einsehen Die im Speicher zugeordneten Dateien in einem Prozess können Sie sich von Process Explorer auflisten lassen. Schalten Sie dazu den unteren Bereich in die DLL-Ansicht um (indem Sie View > Lower Pane View > DLLs wählen). Was Sie dann sehen, ist jedoch nicht nur eine Liste von DLLs, sondern aller im Speicher zugeordneten Dateien im Prozessadressraum. Einige davon sind DLLs, eine ist die ausgeführte Abbilddatei (EXE), und darüber hinaus kann es noch weitere Einträge für im Speicher zugeordnete Datendateien geben. Die folgende Ausgabe von Process Explorer zeigt einen WinDbg-Prozess, der mehrere Speicherzuordnungen verwendet, um auf die untersuchte Speicherauszugsdatei zuzugreifen. Wie die meisten Windows-Programme verwendet auch WinDb (oder eine der Windows-DLLs, die es verwendet) die Speicherzuordnung, um auf die Windows-Datendatei Locale.nls zuzugreifen, die zur Windows-Unterstützung für die Internationalisierung gehört. → Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 359
Sie können auch nach im Speicher zugeordneten Dateien suchen, indem Sie das Menü Find öffnen und Find Handle or DLL auswählen (oder (Strg) + (F) drücken). Das kann praktisch sein, wenn Sie herauszufinden versuchen, welche Prozesse eine DLL oder eine im Speicher zugeordnete Datei verwenden, die Sie gerade ersetzen möchten. Speicherschutz Wie bereits in Kapitel 1 erwähnt, bietet Windows einen Speicherschutz, sodass kein Benutzerprozess versehentlich oder absichtlich den Adressraum eines anderen Prozesses oder des Betriebssystems beschädigen kann. Dieser Schutz erfolgt auf die vier folgenden Weisen: QQ Alle systemweiten Datenstrukturen und Speicherpools, die von Kernelmodus-Systemkomponenten verwendet werden, sind nur im Kernelmodus zugänglich. Benutzermodus­ threads können auf diese Seiten nicht zugreifen. Wenn sie es versuchen, ruft die Hardware einen Fehler hervor, den der Speicher-Manager dem Thread als Zugriffsverletzung meldet. QQ Jeder Prozess hat seinen eigenen privaten Adressraum, der gegen den Zugriff durch Threads anderer Prozesse geschützt ist. Auch gemeinsam genutzter Speicher bildet keine Ausnahme, da Prozesse auf diese gemeinsamen Regionen mithilfe von Adressen zugreifen, die jeweils zu ihrem eigenen virtuellen Adressraum gehören. Die einzige tatsächliche Ausnahme liegt vor, wenn ein anderer Prozess Lese- oder Schreibzugriff auf das Prozessobjekt oder das Recht SeDebugPrivilege hat und daher die Funktion ReadProcessMemory oder WriteProcessMemory verwenden kann. Wenn ein Thread auf eine Adresse verweist, greifen die Hardware für den virtuellen Speicher und der Speicher-Manager ein und übersetzen diese virtuelle Adresse in eine physische. Durch die Steuerung dieser Übersetzung kann Windows garantieren, dass Threads in einem Prozess nicht unangemessen auf eine Seite eines anderen Prozesses zugreifen. QQ Zusätzlich zu dem impliziten Schutz durch die Übersetzung von virtuellen in physische Adresse bieten alle von Windows unterstützten Prozessoren auch einen hardwaregestützten Speicherschutz wie Lese/Schreib-Zugriff, reinen Lesezugriff usw., wobei die Einzelheiten vom jeweiligen Prozessor abhängen. Beispielsweise sind Codeseiten im Adressraum eines 360 Kapitel 5 Speicherverwaltung
Prozesses als schreibgeschützt (read-only) gekennzeichnet, sodass sie von anderen Benutzerthreads nicht geändert werden können. Tabelle 5–2 führt die in der Windows-API definierten Speicherschutzoptionen auf (siehe auch die Dokumentation für die Funktionen VirtualProtect, VirtualProtectEx, VirtualQuery und VirtualQueryEx). QQ Abschnittsobjekte für die gemeinsame Nutzung des Speichers verfügen über reguläre Windows-Zugriffssteuerungslisten. Sie werden überprüft, wenn ein Prozess versucht, auf den Abschnitt zuzugreifen, was den Zugriff auf den gemeinsamen Speicher auf die Prozesse mit den entsprechenden Rechten beschränkt. Die Zugriffssteuerung kommt auch ins Spiel, wenn ein Thread einen Abschnitt für eine zugeordnete Datei erstellen möchte. Dazu muss der Thread mindestens Lesezugriff für das zugrunde liegende Dateiobjekt haben. Hat ein Thread ein Handle zu einem Abschnitt geöffnet, unterliegen seine Aktionen immer noch dem Speicher-Manager und den zuvor beschriebenen hardwaregestützten Seitenschutzvorkehrungen. Ein Thread kann den Schutz einzelner virtueller Seiten in einem Abschnitt ändern, wenn dadurch nicht die Berechtigungen in der Zugriffssteuerungsliste für das Abschnittsobjekt verletzt werden. Beispielsweise erlaubt der Speicher-Manager einem Thread, die Seiten eines schreibgeschützten Prozesses auf Kopieren beim Schreiben (Copy-on-Write) zu ändern, aber nicht auf Lese/Schreib-Zugriff. Der Zugriff mit Kopieren beim Schreibvorgang ist gestattet, da er sich nicht auf andere Prozesse auswirkt, die die Daten ebenfalls nutzen. 1 Attribut Beschreibung PAGE_NOACCESS Jeder Versuch, in der Datei zu lesen, zu schreiben oder Code auszuführen, führt zu einer Zugriffsverletzung. PAGE_READONLY Jeder Versuch, in der Datei zu schreiben (und auf Prozessoren mit Ausführungsschutz auch, Code auszuführen), führt zu einer Zugriffsverletzung. Lesevorgänge dagegen sind erlaubt. PAGE_READWRITE Die Seite kann gelesen und geschrieben, aber nicht ausgeführt werden. PAGE_EXECUTE Jeder Versuch, in Code in der Region zu schreiben, führt zu einer Zugriffsverletzung. Ausführungsvorgänge (und Lesevorgänge auf allen existierenden Prozessoren) dagegen sind erlaubt. PAGE_EXECUTE_READ1 Jeder Versuch, in diese Region zu schreiben, führt zu einer Zugriffsverletzung. Ausführungs- und Lesevorgänge dagegen sind erlaubt. PAGE_EXECUTE_READWRITE1 Die Seite kann gelesen, geschrieben und ausgeführt werden. Alle Zugriffsversuche haben Erfolg. PAGE_WRITECOPY Wenn ein Prozess versucht, in diese Region zu schreiben, stellt ihm das System eine private Kopie der Seite zur Verfügung. Auf Prozessoren mit Ausführungsschutz1 führen Versuche, Code in diesem Bereich auszuführen, zu einer Zugriffsverletzung. → Der Ausführungsschutz ist auf Prozessoren mit der entsprechenden Hardwareunterstützung möglich (u. a. auf allen x64-Prozessoren), aber nicht auf älteren x86-Prozessoren. Ohne diese Unterstützung wird aus dem Ausführungs- ein Leseschutz. Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 361
Attribut Beschreibung PAGE_EXECUTE_WRITECOPY Wenn ein Prozess versucht, in diese Region zu schreiben, stellt ihm das System eine private Kopie der Seite zur Verfügung. Leseund Ausführungsversuche sind dagegen zulässig. Dabei wird keine Kopie erstellt. PAGE_GUARD Jeglicher Versuch, in einer Wächterseite zu lesen oder zu schreiben, löst die Ausnahme EXCEPTION_GUARD_PAGE aus und schaltet den Wächterseitenstatus ab. Dadurch können Wächterseiten als einmalige Alarmierungsmöglichkeit genutzt werden. Dieses Flag kann mit allen anderen Seitenschutzeinstellungen außer PAGE_­ NOACCESS kombiniert werden. PAGE_NOCACHE Hierbei wird physischer Arbeitsspeicher genutzt, der nicht zwischengespeichert wird. Zur allgemeinen Verwendung wird diese Vorgehensweise nicht empfohlen. Sie kann allerdings für Gerätetreiber nützlich sein, etwa um einen Videoframepuffer ohne Zwischenspeicherung zuzuordnen. PAGE_WRITECOMBINE Hiermit wird das kombinierte Schreiben eingeschaltet. Bei dieser Einstellung führt der Prozessor keine Schreibvorgänge im Cache aus. Zwar kann dadurch der Datenverkehr im Arbeitsspeicher erheblich ansteigen, doch dafür wird versucht, Schreibanforderungen zu bündeln, um die Leistung zu optimieren. Soll beispielsweise mehrmals an derselben Adresse geschrieben werden, so wird nur der letzte dieser Schreibvorgänge ausgeführt. Auch Schreibvorgänge in benachbarten Adressen können dadurch zu einem einzigen langen Schreibvorgang zusammengefasst werden. Für allgemeine Anwendungen wird diese Möglichkeit nicht genutzt, sie kann aber für Gerätetreiber sehr praktisch sein, etwa um einen Videoframepuffer so zuzuordnen, dass kombinierte Schreibvorgänge zulässig sind. PAGE_TARGETS_INVALID und PAGE_­ TARGETS_NO_UPDATE (Windows 10 und Windows Server 2016) Diese Werte steuern das Verhalten des Control Flow Guard (CFG) für ausführbaren Code in den betreffenden Seiten. Beide Konstanten haben denselben Wert, allerdings werden sie in unterschiedlichen Aufrufen verwendet. Im Grunde genommen stellen sie einen Umschalter dar. PAGE_TARGETS_INVALID gibt an, dass CFG bei indirekten Aufrufen fehlschlagen und der Prozess abstürzen soll. PAGE_TARGETS_NO_UPDATE lässt VirtualProtect-Aufrufe zu, die den Seitenbereich ändern, um eine Ausführung zuzulassen, bei der der CFG-Status nicht aktualisiert wird. Weitere Informationen über CFG erhalten Sie in Kapitel 7. Tabelle 5–2 In der Windows-API definierte Speicherschutzoptionen Datenausführungsverhinderung Die Datenausführungsverhinderung (Date Execution Prevention, DEP) oder der Ausführungsschutz (No Execute, NX) ruft bei dem Versuch, die Steuerung an eine Anweisung auf einer als nicht ausführbar gekennzeichneten Seite zu übergeben, einen Zugriffsfehler hervor. Dadurch werden bestimmte Arten von Malware daran gehindert, Schwachstellen im System auszunutzen, indem sie Code auf einer Datenseite wie dem Stack ausführen. Mithilfe der Datenausführungsverhinderung können Sie auch erkennen, wenn unsauber geschriebene Programme die Berechtigungen für Seiten, auf denen Code ausgeführt werden soll, nicht korrekt festlegen. Wird im Kernelmodus versucht, Code auf einer als nicht ausführbar gekennzeichneten Seite auszuführen, stürzt das System mit dem Fehlerprüfcode ATTEMPTED_EXECUTE_OF_NOEXECUTE_­ 362 Kapitel 5 Speicherverwaltung
MEMORY (0xFC) ab. (Eine Erklärung dieser Codes erhalten Sie in Kapitel 15 von Band 2.) Geschieht dies im Benutzermodus, wird die Ausnahme STATUS_ACCESS_VIOLATION (0xC0000005) an den Thread gesendet, der den ungültigen Verweis versucht. Wenn ein Prozess Speicher zuweist, der ausführbar sein soll, muss er die Seiten entsprechend kennzeichnen, indem er bei den Speicherzuweisungsfunktionen auf Seitenebene das Flag PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_­ EXECUTE_READWRITE oder PAGE_EXECUTE_WRITECOPY angibt. Auf 32-Bit-x86-Systemen, die die Datenausführungsverhinderung unterstützen, dient Bit 63 im Seitentabelleneintrag (Page Table Entry, PTE) dazu, eine Seite als nicht ausführbar zu kennzeichnen. Daher steht die Datenausführungsverhinderung nur dann zur Verfügung, wenn der Prozessor im PAE-Modus läuft (physische Adresserweiterung), da die Seitentabelleneinträge sonst nur 32 Bits breit sind (siehe den Abschnitt »Übersetzung virtueller Adressen auf x86-Systemen« weiter hinten in diesem Kapitel). Daher erfordert die Unterstützung für Hardware-DEP auf 32-Bit-Systemen, dass der PAE-Kernel geladen wird (%SystemRoot%\System32\ Ntkrnlpa.exe), der zurzeit der einzige unterstützte Kernel auf x86-Systemen ist. Auf ARM-Systemen ist die Datenausführungsverhinderung stets eingeschaltet. Auf 64-Bit-Versionen von Windows wird der Ausführungsschutz automatisch auf alle 64-Bit-Prozesse und Gerätetreiber angewendet. Ausschalten können Sie ihn nur, indem Sie die BCD-Option nx auf AlwaysOff setzen. Der Ausführungsschutz für 32-Bit-Programme hängt von der im nächsten Absatz beschriebenen Systemkonfiguration ab. Auf 64-Bit-Windows wird der Ausführungsschutz auf Threadstacks (sowohl im Benutzer- als auch im Kernelmodus), auf nicht eigens als ausführbar gekennzeichnete Benutzermodusseiten, auf den ausgelagerten Kernelpool und den Kernelsitzungspool angewendet. Eine Beschreibung von Kernelspeicherpools finden Sie im Abschnitt »Kernelmodusheaps (Systemspeicherpools)«. Auf 32-Bit-Windows dagegen wird der Ausführungsschutz nur auf Threadstacks und Benutzermodusseiten angewendet, nicht aber auf den ausgelagerten und den Sitzungspool. Die Anwendung des Anwendungsschutzes auf 32-Bit-Prozesse hängt von dem Wert der BCD-Option nx ab. Um diese Einstellung zu ändern, rufen Sie die Registerkarte Datenausführungsverhinderung im Dialogfeld Leistungsoptionen auf (siehe Abb. 5–3). Dieses Dialogfeld erreichen Sie, indem Sie auf Computer rechtsklicken, Eigenschaften auswählen, auf Erweiterte Systemeinstellungen klicken und dann Leistungseinstellungen wählen. Wenn Sie in diesem Dialogfeld den Ausführungsschutz einrichten, wird die BCD-Option nx auf den entsprechenden Wert gesetzt. Tabelle 5–3 zeigt, welche Einstellungen auf der Registerkarte Datenausführungsverhinderung welchen nx-Werten entsprechen. 32-Bit-Anwendungen, die von dem Ausführungsschutz ausgenommen sind, werden im Registrierungsschlüssel HKLM\SOFTWARE\Microsoft\ Windows NT\CurrentVersion\AppCompatFlags\Layers aufgeführt, wobei als Name des Wertes der vollständige Pfad der ausführbaren Datei angegeben ist und als Wert DisableNXShowUI. Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 363
Abbildung 5–3 Einstellungen auf der Registerkarte Datenausführungsverhinderung nx-Wert Option auf der Registerkarte Datenausführungsverhinderung Bedeutung OptIn Datenausführungsverhinderung nur für erforderliche Windows-Programme und -Dienste aktivieren Die Datenausführungsverhinderung wird für die Kernsystemabbilder von Windows aktiviert. 32-Bit-Prozesse können die Datenausführungsverhinderung für ihre gesamte Lebensdauer dynamisch einstellen. OptOut Datenausführungsverhinderung für alle Programme und Dienste mit Ausnahme der ausgewählten aktivieren Die Datenausführungsverhinderung wird für ausführbare Dateien mit Ausnahme der eigens angegebenen aktiviert. 32-Bit-Prozesse können die Datenausführungsverhinderung für ihre gesamte Lebensdauer dynamisch einstellen. Diese Einstellung aktiviert auch Systemkompatibilitätskorrekturen für die Datenausführungsverhinderung. AlwaysOn Für diese Einstellung gibt es keine Option in dem Dialogfeld. Die Datenausführungsverhinderung wird für alle Komponenten aktiviert, wobei es keine Möglichkeit gibt, einzelne Anwendungen auszunehmen. Die dynamische Konfiguration für 32-Bit-Prozesse und die Systemkompatibilitätskorrekturen werden ausgeschaltet. AlwaysOff Für diese Einstellung gibt es keine Option in dem Dialogfeld. Die Datenausführungsverhinderung wird ausgeschaltet. (Von dieser Einstellung wird abgeraten.) Auch die dynamische Konfiguration für 32-Bit-Prozesse wird ausgeschaltet. Tabelle 5–3 Werte der BCD-Option nx 364 Kapitel 5 Speicherverwaltung
Auf Windows-Clientversionen (sowohl 64 als auch 32 Bit) wird der Ausführungsschutz für 32-Bit-Prozesse in der Standardeinstellung nur auf ausführbare Dateien des Betriebssystems angewendet. Die Option nx ist also auf OptIn gesetzt. Dadurch ist dafür gesorgt, dass 32-Bit-Anwendungen, die Code auf nicht ausdrücklich als ausführbar gekennzeichneten Seiten ausführen müssen, korrekt funktionieren, beispielsweise selbstextrahierende oder gepackte Anwendungen. Auf Windows-Serversystemen dagegen gilt der Ausführungsschutz für 32-Bit-Anwendungen für alle 32-Bit-Programme, hier ist die Option nx auf OptOut gesetzt. Auch wenn Sie die Aktivierung der Datenausführungsverhinderung erzwingen, können Anwendungen diese Einrichtung für ihre eigenen Abbilder ausschalten. Beispielsweise überprüft der Abbildlader unabhängig von den festgelegten Ausführungsschutzoptionen die Sig­ natur einer ausführbaren Datei auf bekannte Kopierschutzmechanismen (wie SafeDisc oder SecuROM) und deaktiviert den Ausführungsschutz, um die Kompatibilität mit älterer kopiergeschützter Software (z. B. älteren Computerspielen) zu gewährleisten. (Mehr über den Abbildlader erfahren Sie in Kapitel 3.) Experiment: Die Einstellungen für die Datenausführungsverhinderung ermitteln Process Explorer kann den aktuellen Status der Datenausführungsverhinderung für alle Prozesse auf Ihrem System anzeigen. Dabei können Sie auch erkennen, ob ein Prozess standardmäßig geschützt ist oder ob der Schutz für ihn ausdrücklich aktiviert wurde. Rechtsklicken Sie auf den Prozessbaum, wählen Sie Select Columns und dann auf der Registerkarte Process Image die Spalte DEP Status. Sie kann die folgenden drei Werte haben: QQ DEP (permanent) Die Datenausführungsverhinderung ist für den Prozess aktiviert, weil er zu den erforderlichen Windows-Programmen oder -Diensten gehört. QQ DEP Die Datenausführungsverhinderung wurde für den Prozess ausführlich eingeschaltet. Das kann aufgrund einer systemweiten Richtlinie für alle 32-Bit-Prozesse geschehen sein, durch einen API-Aufruf wie SetProcessDEPPolicy oder durch Setzen des Linkerflags /NXCOMPAT beim Erstellen des Abbilds. QQ Keine Anzeige Wenn in der Spalte keine Informationen über den Prozess angezeigt werden, ist die Datenausführungsverhinderung aufgrund einer systemweiten Richtlinie, eines ausdrücklichen API-Aufrufs oder eines Shims ausgeschaltet. Um Kompatibilität mit älteren Versionen (Version 7.1 oder früher) des ATL-Frameworks zu bieten (Active Template Library), bietet der Windows-Kernel auch eine Umgebung zur ATL-Thunk­ emulation. Sie erkennt ATL-Thunkcodesequenzen, die eine DEP-Ausnahme ausgelöst haben, und emuliert die erwartete Operation. Bei der Verwendung des neuesten Microsoft-C++-Compilers können Anwendungsentwickler durch Angabe des Flags /NXCOMPAT (das wiederum das Flag IMAGE_DLLCHARACTERISTICS_NX_COMPAT im PE-Header setzt) verhindern, dass die ATL-Thunk­ emulation angewendet wird. Dieses Flag teilt dem System mit, dass die ausführbare Datei die Datenausführungsverhinderung vollständig unterstützt. Bei der Einstellung AlwaysOn ist die ATL-Thunkemulation komplett ausgeschaltet. Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 365
Wenn das System bei der Einstellung OptIn oder OptOut einen 32-Bit-Prozess ausführt, kann dieser die Datenausführungsverhinderung mit der Funktion SetProcessDEPPolicy dynamisch ausschalten oder dauerhaft einschalten. Wird die Datenausführungsverhinderung durch diese API eingeschaltet, kann sie während der Lebensdauer des Prozesses nicht programmgesteuert ausgeschaltet werden. Mit dieser Funktion ist es auch möglich, die ATL-Thunkemulation dynamisch auszuschalten, wenn das Abbild nicht mit dem Flag /NXCOMPAT kompiliert wurde. Bei 64-Bit-Prozessen und auf Systemen, die mit der Einstellung AlwaysOff oder AlwaysOn gestartet wurden, schlägt die Funktion immer fehl. Die Funktion GetProcessDEPPolicy gibt die 32-Bit-DEP-Richtlinie für einen Prozess zurück. Auf 64-Bit-Systemen schlägt sie fehl, aber dort ist die Richtlinie auch immer gleich – die Datenausführungsverhinderung ist stets eingeschaltet. Mit GetSystemDEPPolicy können Sie die systemweiten Richtlinienwerte aus Tabelle 5–3 abrufen. Kopieren beim Schreiben Zum Kopieren beim Schreiben (Copy-on-Write) wendet der Speicher-Manager eine Optimierung an, um physischen Arbeitsspeicher zu sparen. Wenn ein Prozess eine entsprechend geschützte Sicht eines Abschnittsobjekts zuordnet, die les- und schreibbare Seiten enthält, verzögert der Speicher-Manager das Kopieren der Seiten, bis tatsächlich auf die Seite geschrieben wird, anstatt schon gleich bei der Zuordnung der Sicht eine private Kopie für den Prozess anzulegen. In dem Beispiel aus Abb. 5–4 sehen Sie zwei Prozesse, die sich drei Seiten teilen. Alle drei Seiten sind für das Kopieren beim Schreiben gekennzeichnet, aber zurzeit hat noch keiner der beiden Prozesse versucht, irgendwelche Daten auf diesen Seiten zu ändern. Prozess A RAM Seite 1 Originaldaten Seite 2 Prozess B Originaldaten Originaldaten Originaldaten Originaldaten Originaldaten Seite 3 Abbildung 5–4 Kopieren beim Schreiben – vorher Wenn ein Thread in einem der beiden Prozesse auf eine Seite schreibt, wird ein Speicherverwaltungsfehler ausgelöst. Der Speicher-Manager erkennt, dass es sich um eine Copy-on-Write-­ Seite handelt, weshalb er den Fehler nicht als Zugriffsverletzung meldet, sondern wie folgt vorgeht: 1. Er weist eine neue les- und schreibbare Seite im physischen Arbeitsspeicher zu. 2. Er kopiert den Inhalt der Originalseite in die neue Seite. 366 Kapitel 5 Speicherverwaltung
3. Er aktualisiert die zugehörigen Seitenzuordnungsinformationen in dem Prozess (mehr darüber weiter hinten in diesem Kapitel), sodass sie auf den neuen Speicherort zeigen. 4. Er verwirft die Ausnahme und sorgt dafür, dass die Anweisung, die den Fehler hervorgerufen hat, erneut ausgeführt wird. Bei diesem zweiten Versuch hat die Schreiboperation Erfolg. Wie Sie in Abb. 5–5 sehen, ist die Kopie aber eine private Seite des schreibenden Prozesses und daher für den anderen Prozess nicht sichtbar, der immer noch die gemeinsam genutzte Originalseite verwendet. Jeder weitere Prozess, der auf dieselbe gemeinsame Seite schreibt, erhält ebenfalls seine private Kopie. Prozess A RAM Seite 1 Originaldaten Seite 2 Prozess B Originaldaten Geänderte Daten Originaldaten Originaldaten Originaldaten Seite 3 Kopie von Seite 2 mit Änderungen Abbildung 5–5 Kopieren beim Schreiben – nachher Das Kopieren beim Schreiben wird unter anderem verwendet, um Haltepunkte in Debuggern zu ermöglichen. Beispielsweise sind Codeseiten zu Anfang standardmäßig rein für die Ausführung da. Wenn ein Programmierer beim Debuggen einen Haltepunkt setzt, muss der Debugger die Anweisung dafür in den Code einfügen. Dazu stellt er zunächst den Schutz der Seite auf PAGE_EXECUTE_READWRITE um und ändert dann den Anweisungsstream. Da die Codeseite zu einem zugeordneten Bereich gehört, erstellt der Speicher-Manager für den Prozess eine private Kopie, in der der Haltepunkt gesetzt ist, während die anderen Prozesse mit der nicht geänderten Codeseite weitermachen. Das Kopieren beim Schreiben ist ein Beispiel für die Technik der verzögerten Auswertung, die der Speicher-Manager so oft wie möglich einsetzt. Algorithmen, die diese Technik nutzen, vermeiden kostspielige Operationen, bis sie absolut notwendig sind. Wird die Operation niemals erforderlich, so wird keine Zeit darauf verschwendet. Um die durch Kopieren beim Schreiben hervorgerufenen Fehler zu untersuchen, schauen Sie sich in der Leistungsüberwachung den Leistungsindikator Schreibkopien/s aus der Kategorie Arbeitsspeicher an. AWE (Address Windowing Extensions) Die 32-Bit-Version von Windows unterstützt zwar bis zu 64 GB an physischem Arbeitsspeicher (siehe Tabelle 2–2), doch die einzelnen 32-Bit-Benutzerprozesse verfügen standardmäßig nur über einen virtuellen Adressraum von jeweils 2 GB. (Wie im Abschnitt »Layouts für virtuelle Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 367
­Adressräume« weiter hinten in diesem Kapitel erklärt, können Sie diesen Wert mit der BCD-Option increaseuserva auf bis zu 3 GB anheben.) Eine Anwendung, die mehr als 2 GB (bzw. 3 GB) an leicht verfügbaren Daten in einem einzigen Prozess benötigt, kann dazu die Dateizuordnung nutzen und immer wieder Teile ihres Adressraums verschiedenen Teilen einer großen Datei zuordnen. Allerdings ist jede dieser Neuzuordnungen mit einer erheblichen Auslagerung verbunden. Um eine höhere Leistung und eine feinere Steuerung zu ermöglichen, bietet Windows die sogenannten AWE-Funktionen an (Address Windowing Extensions). Damit ist ein Prozess in der Lage, mehr physischen Arbeitsspeicher zuzuweisen, als sein virtueller Adressraum darstellen kann. Der Zugriff auf den physischen Speicher erfolgt dann, indem der Prozess Teile seines virtuellen Adressraums im Laufe der Zeit verschiedenen Teilen des physischen Arbeitsspeichers zuweist. Um Arbeitsspeicher mithilfe der AWE-Funktionen zuzuweisen und zu nutzen, müssen Sie die drei folgenden Schritte ausführen: 1. Sie weisen den zu verwendenden physischen Arbeitsspeicher zu. Die Anwendung verwendet dazu die Windows-Funktion AllocateUserPhysicalPages oder AllocateUserPhysicalPagesNuma, für die das Recht SeLockMemoryPrivilege erforderlich ist. 2. Sie erstellen im virtuellen Adressraum eine oder mehrere Regionen, die als Fenster zur Zuordnung von Sichten des physischen Arbeitsspeichers dienen. Dazu verwendet die Anwendung die Win32-Funktion VirtualAlloc, VirtualAllocEx oder VirtualAllocExNuma mit dem Flag MEM_PHYSICAL. 3. Schritt 1 und 2 bilden allgemein ausgedrückt die Initialisierung. Um den Speicher tatsächlich nutzen zu können, ordnet die Anwendung mit MapUserPhysicalPages oder MapUserPhysicalPagesScatter einen Teil der in Schritt 1 zugewiesenen physischen Region zu einer der in Schritt 2 zugewiesenen virtuellen Regionen oder Fenster zu. Ein Beispiel dafür sehen Sie in Abb. 5–6. Hier hat die Anwendung in ihrem Adressraum ein Fenster von 256 MB erstellt und 4 GB physischen Arbeitsspeicher zugeordnet. Mit MapUserPhy­ sicalPages oder MapUserPhysicalPagesScatter kann sie nun auf jeden Teil des physischen Speichers zugreifen, indem sie den gewünschten Teil des Arbeitsspeichers in dem 256-MB-Fenster zuordnet. Die Größe des Fensters bestimmt die Menge des physischen Speichers, auf den die Anwendung in einer gegebenen Zuordnung zugreifen kann. Um Zugang zu einem anderen Teil des zugewiesenen RAM zu erhalten, ordnet die Anwendung den Bereich einfach neu zu. 368 Kapitel 5 Speicherverwaltung
Systemadress­raum AWE-Speicher AWE-Fenster Benutzer­ adress­raum Abbildung 5–6 Zuweisung von physischem Arbeitsspeicher mit AWE Die AWE-Funktionen sind in allen Windows-Editionen vorhanden und können unabhängig davon genutzt werden, über wie viel physischen Speicher das System verfügt. Besonders nützlich ist AWE allerdings auf 32-Bit-Systemen mit mehr als 2 GB physischem Arbeitsspeicher, da es 32-Bit-Prozessen die Möglichkeit gibt, mehr RAM zuzuweisen, als ihr virtueller Adressraum normalerweise zulässt. AWE wird auch aus Sicherheitsgründen eingesetzt. Da AWE-Speicher niemals ausgelagert wird, werden Daten im AWE-Speicher niemals in die Auslagerungsdatei kopiert, die jemand durch einen Neustart in einem anderen Betriebssystem einsehen könnte. (VirtualLock bietet dieselbe Garantie für Seiten im Allgemeinen.) Arbeitsspeicher, der mit AWE-Funktionen zugewiesen und zugeordnet wurde, unterliegt jedoch folgenden Einschränkungen: QQ Die Seiten können nicht von mehreren Prozessen gemeinsam genutzt werden. QQ Eine physische Seite kann nicht zu mehr als einer virtuellen Adresse zugeordnet werden. QQ Der Seitenschutz ist auf »Lesen und Schreiben«, »nur Lesen« und »kein Zugriff« eingeschränkt. Auf 64-Bit-Systemen darf jeder Prozess einen virtuellen Adressraum von 128 TB haben, aber nur 24 TB RAM (auf Windows Server 2016). Daher wird AWE hier nicht gebraucht, um einer Anwendung mehr RAM zu erlauben, als in ihren virtuellen Adressraum passt, denn schließlich ist der RAM für einen Prozess immer kleiner als sein virtueller Prozessraum. Allerdings ist AWE nach wie vor nützlich, um im Adressraum des Prozesses Regionen festzulegen, die nicht ausgelagert werden sollen. AWE bietet auch eine feinere Granularität als die Dateizuordnungs-APIs (da die Systemseitengröße 4 KB statt 64 KB beträgt). Eine Beschreibung der Datenstrukturen für die Seitentabellen, die zur Zuordnung von Arbeitsspeichern auf Systemen mit mehr als 4 GB an physischem Arbeitsspeicher verwendet werden, finden Sie im Abschnitt »Übersetzung virtueller Adressen auf x86-Systemen«. Vom Speicher-Manager bereitgestellte Dienste Kapitel 5 369
Kernelmodusheaps (Systemspeicherpools) Bei der Initialisierung des Systems erstellt der Speichermanager zwei Speicherpools dynamischer Größe (Heaps), die von den meisten Kernelmoduskomponenten zur Zuordnung von Systemarbeitsspeicher verwendet werden: QQ Nicht ausgelagerter Pool Er besteht aus einem Bereich von virtuellen Systemadressen, die sich garantiert ständig im physischen Arbeitsspeicher befinden und daher jederzeit zugänglich sind, ohne Seitenfehler hervorzurufen. Das heißt, der Zugriff auf sie kann von jeder IRQL aus erfolgen. Ein nicht ausgelagerter Pool ist unter anderem deshalb erforderlich, da Seitenfehler auf DPC-/Dispatchebene und darüber nicht gehandhabt werden können. Daher müssen sich jeglicher Code und jegliche Daten, die auf dieser Ebene oder darüber ausgeführt oder abgerufen werden sollen, in einem Teil des Arbeitsspeichers befinden, der nicht ausgelagert werden kann. QQ Ausgelagerter Pool Diese Region im virtuellen Arbeitsspeicher des Systems kann ausgelagert und wieder zurückgeholt werden. Gerätetreiber, die keinen Speicherzugriff auf DPC-/Dispatchebene oder darüber benötigen, können den ausgelagerten Pool verwenden. Er ist von jedem Prozesskontext aus zugänglich. Beide Speicherpools befinden sich im Systemteil des Adressraums und werden im virtuellen Adressraum jedes Prozesses zugeordnet. Die Exekutive stellt Routinen für die Zuweisung aus diesen Pools und die Aufhebung der Zuweisung bereit. Um mehr darüber zu erfahren, suchen Sie in der Dokumentation des WDK nach Funktionen, deren Namen mit ExAllocatePool und ExFreePool beginnen. Zu Anfang verfügt ein System über vier ausgelagerte Pools, die zum gesamten ausgelagerten Pool des Systems kombiniert werden, sowie zwei nicht ausgelagerte Pools. Je nach Anzahl der NUMA-Knoten auf dem System werden weitere Pools bis zu einer Höchstzahl von 64 erstellt. Das Vorhandensein von mehr als einem ausgelagerten Pool verringert die Häufigkeit von Systemcodeblockierungen bei gleichzeitigem Aufruf von Poolroutinen. Außerdem werden die Pools verschiedenen virtuellen Adressbereichen zugeordnet, die den NUMA-Knoten des Systems entsprechen. Auch die Datenstrukturen zur Beschreibung der Poolzuweisungen, z. B. die umfangreichen Look-Aside-Listen, werden den einzelnen NUMA-Knoten zugeordnet. Neben den ausgelagerten und nicht ausgelagerten Pools gibt es noch weitere Pools mit besonderen Eigenschaften oder Verwendungszwecken. Beispielsweise gibt es im Sitzungsraum eine Poolregion für Daten, die von allen Prozessen einer Sitzung gemeinsam genutzt werden. Ein anderes Beispiel ist der besondere Pool. Zuweisungen daraus sind von Seiten umgeben, die mit »kein Zugriff« gekennzeichnet sind, um Probleme in dem Code zu isolieren, der auf Speicher zugreift, bevor oder nachdem die Region des Pools zugewiesen wurde. Poolgrößen Ein nicht ausgelagerter Pool hat zu Anfang eine Größe, die vom physischen Arbeitsspeicher auf dem System abhängt, und wächst dann nach Bedarf. Die Anfangsgröße beträgt gewöhnlich 3 % des System-RAM. Wenn das jedoch einen Wert von weniger als 40 MB ergibt, verwendet das System 40 MB als Anfangsgröße. Sollten diese 40 MB jedoch auch mehr als 10 % sein, 370 Kapitel 5 Speicherverwaltung
bekommt der Pool eine Anfangsgröße von 10 % des RAM. Windows bestimmt dynamisch die Maximalgröße und erlaubt jedem Pool, von seiner Anfangsgröße bis zu dem in Tabelle 5–4 genannten Maximum zu wachsen. Maximum auf 64-Bit-Systemen (Windows 8.1, Windows 10, Windows Server 2012 R2, Windows Server 2016) Art des Pools Maximum auf 32-BitSystemen Maximum auf 64-BitSystemen (Windows 8, Windows Server 2012) Nicht ausgelagert 75 % des physischen Speichers oder 2 GB (der kleinere Wert) 75 % des physischen Speichers oder 128 GB (der kleinere Wert) 16 TB Ausgelagert 2 GB 384 GB 15,5 TB Tabelle 5–4 Maximale Poolgrößen Vier dieser berechneten Werte werden in Windows 8.x und in Windows Server 2012 und 2012 R2 in Kernelvariablen gespeichert. Drei davon können über Leistungsindikatoren eingesehen werden, und einer wird als Leistungsindikatorenwert berechnet. In Windows 10 und Windows Server 2016 wurden die globalen Variablen in Felder der globalen Speicherverwaltungsstruktur MiState (vom Typ MI_SYSTEM_INFORMATION) verlagert. Darin befindet sich die Variable Vs (vom Typ _MI_VISIBLE_STATE) mit den Informationen. Die globale Variable MiVisibleState zeigt ebenfalls auf das Element Vs. Tabelle 5–5 gibt einen Überblick über diese Variablen und Indikatoren. Kernelvariable Leistungsindikator Beschreibung MmSizeOfNonPagedPoolInBytes Arbeitsspeicher: Nicht-Auslagerungsseiten (Bytes) Dies ist die Größe des ursprünglich nicht ausgelagerten Pools. Er kann vom System je nach den Speicheranforderungen automatisch verkleinert oder vergrößert werden. Der Leistungsindikator spiegelt diese Änderungen wider, die Kernelvariable jedoch nicht. MmMaximumNonPagedPoolInBytes (Windows 8.x und Server 2012/R2) Nicht verfügbar Dies ist die maximale Größe eines nicht ausgelagerten Pools. MiVisibleState->MaximumNonPagePoolInBytes (Windows 10 und Server 2016) Nicht verfügbar Dies ist die maximale Größe eines nicht ausgelagerten Pools. Nicht verfügbar Arbeitsspeicher: Auslagerungsseiten (Bytes) Dies ist die aktuelle virtuelle Gesamtgröße des ausgelagerten Pools. WorkingSetSize (Anzahl der Seiten) in der Struktur MmPagedPoolWs (Typ MMSUPPORT) (Windows 8.x und Server 2012/R2) Arbeitsspeicher: Auslagerungsseiten: Residente Bytes Dies ist die aktuelle physische (residente) Größe des ausgelagerten Pools. MmSizeOfPagedPoolInBytes (Windows 8.x und Server 2012/R2) Nicht verfügbar Dies ist die maximale (virtuelle) Größe eines ausgelagerten Pools. MiState.Vs.SizeOfPagedPoolInBytes (Windows 10 und Server 2016) Nicht verfügbar Dies ist die maximale (virtuelle) Größe eines ausgelagerten Pools. Tabelle 5–5 Variablen und Leistungsindikatoren für die Systempoolgrößen Kernelmodusheaps (Systemspeicherpools) Kapitel 5 371
Experiment: Die maximalen Poolgrößen bestimmen Die Höchstgrößen der Pools können Sie mit Process Explorer und durch ein Live-Kerneldebugging (siehe Kapitel 1) bestimmen. In Process Explorer wählen Sie dazu im Menü View den Punkt System Information und klicken auf die Registerkarte Memory. Die Poolgrenz­ werte werden im Abschnitt Kernel Memory angezeigt: Damit Process Explorer diese Informationen abrufen kann, muss er Zugriff auf die Symbole für den Kernel des Systems haben. Wie Sie Process Explorer entsprechend einrichten, erfahren Sie in dem Experiment »Einzelheiten über Prozesse mit Process Explorer einsehen« in Kapitel 1. Im Kerneldebugger verwenden Sie den in diesem Kapitel bereits vorgestellten Befehl !vm. Die Poolnutzung überwachen Das Leistungsobjekt Arbeitsspeicher umfasst mehrere Indikatoren für den nicht ausgelagerten und den ausgelagerten Pool (sowohl virtuell als auch physisch). Außerdem können Sie mit dem Programm Poolmon (aus dem Verzeichnis WDK Tools) die Nutzung des ausgelagerten und des nicht ausgelagerten Pools detailliert beobachten. Eine Beispielausgabe dieses Programms sehen Sie in Abb. 5–7. 372 Kapitel 5 Speicherverwaltung
Abbildung 5–7 Ausgabe von Poolmon Zeilen, die sich geändert haben, werden unterlegt dargestellt. Diese Hervorhebung können Sie durch Eingabe von l während der Ausführung von Poolmon ein- und ausschalten. Durch Eingabe von ? können Sie den Hilfebildschirm aufrufen. Außerdem können Sie festlegen, welche Pools Sie überwachen wollen (der ausgelagerte, der nicht ausgelagerte oder beide), und die Sortierreihenfolge auswählen. Beispielsweise können Sie P eingeben, um nur Zuweisungen aus dem nicht ausgelagerten Pool anzeigen zu lassen, und dann mit D nach der Spalte Diff (Differenz) sortieren, um herauszufinden, welche Strukturen im nicht ausgelagerten Pool am zahlreichsten vorkommen. Neben diesen Befehlen werden auf dem Hilfebildschirm auch die Befehlszeilenoptionen angezeigt, mit denen Sie die Überwachung auf bestimmte Tags (oder auf alle Tags außer einem) einschränken können. Beispielsweise werden bei poolmon -iCM nur CM-Tags überwacht (Zuweisungen des Konfigurations-Managers, der die Registrierung verwaltet). Die Bedeutung der Spalten wird in Tabelle 5–6 erklärt. Spalte Bedeutung Tag Ein 4-Byte-Tag zur Kennzeichnung der Poolzuweisung Type Die Art des Pools (ausgelagert oder nicht ausgelagert) Allocs Die Anzahl aller Zuweisungen. Die Zahl in Klammern zeigt den Unterschied zu dem Wert vor der letzten Aktualisierung an. Frees Die Anzahl aller Freigaben. Die Zahl in Klammern zeigt den Unterschied zu dem Wert vor der letzten Aktualisierung an. Diff Die Anzahl der Zuweisungen abzüglich der Freigaben Bytes Die Gesamtanzahl aller von diesem Tag verbrauchten Bytes. Die Zahl in Klammern zeigt den Unterschied zu dem Wert vor der letzten Aktualisierung an. Per Alloc Die Größe eines einzelnen Vorkommens dieses Tags in Bytes Tabelle 5–6 Poolmon-Spalten Kernelmodusheaps (Systemspeicherpools) Kapitel 5 373
Was die von Windows verwendeten Pooltags bedeuten, erfahren Sie in der Datei Pooltag. txt. Sie befindet sich im Unterverzeichnis Triage des Speicherorts für die Debugging-Tools für Windows. Die Pooltags der Gerätetreiber von Drittanbietern sind darin jedoch nicht verzeichnet, allerdings können Sie bei der im WDK enthaltenen 32-Bit-Version von Poolmon mit dem Schalter -c die lokale Pooltagdatei Localtag.txt erstellen, die sämtliche von Treibern auf dem System verwendeten Pooltags enthält, einschließlich derjenigen von Drittanbietertreibern. Wurde die Binärdatei eines Gerätetreibers nach dem Laden gelöscht, werden ihre Pooltags jedoch nicht mehr erkannt. Alternativ können Sie auch mit dem Sysinternals-Tool Strings.exe nach den Gerätetreibern für ein Pooltag suchen. Beispielsweise gibt der folgende Befehl die Namen der Treiber aus, die den String "abcd" enthalten: strings %SYSTEMROOT%\system32\drivers\*.sys | findstr /i "abcd" Gerätetreiber müssen sich nicht unbedingt in %SystemRoot%\System32\Drivers befinden, sondern können in jedem beliebigen Ordner stecken. Um die vollständigen Pfade aller geladenen Treiber aufzulisten, gehen Sie wie folgt vor: 1. Öffnen Sie das Startmenü und geben Sie Msinfo32 ein. Daraufhin wird Systeminforma­ tionen angezeigt. 2. Führen Sie Systeminformationen aus. 3. Erweitern Sie Softwareumgebung. 4. Markieren Sie Systemtreiber. Gerätetreiber, die zwar geladen, aber vom System gelöscht wurden, werden hier nicht aufgeführt. Eine andere Möglichkeit, um die Nutzung der Pools durch die Gerätetreiber zu beobachten, bietet die in Kapitel 6 erklärte Poolverfolgungsfunktion der Treiberüberprüfung. Dabei ist es zwar nicht mehr nötig, die Pooltags mit den Gerätetreibern zu verknüpfen, allerdings ist ein Neustart erforderlich, um die Treiberüberprüfung für die gewünschten Treiber einzuschalten. Nach dem Neustart mit aktivierter Poolverfolgung können Sie den grafischen Treiberüberprüfungs-Manager (%SystemRoot%\System32\Verifier.exe) verwenden, Informationen über die Poolnutzung aber auch mit dem Befehl Verifier /Log in eine Datei schreiben. Auch mit dem Kerneldebuggerbefehl !poolused können Sie sich einen Überblick über die Poolnutzung verschaffen. Der Befehl !poolused 2 zeigt die Nutzung des nicht ausgelagerten Pools an, wobei die Tags mit der stärksten Nutzung oben stehen. Mit !poolused 4 geben Sie die Nutzung des ausgelagerten Pools in der gleichen Sortierung an. Das folgende Beispiel zeigt Teilausgaben beider Befehle: lkd> !poolused 2 ........ Sorting by NonPaged Pool Consumed NonPaged Paged Tag 374 Allocs Used Allocs Used File 626381 260524032 0 0 File objects Ntfx 733204 227105872 0 0 General Allocation , Binary: ntfs.sys Kapitel 5 Speicherverwaltung
MmCa 513713 148086336 0 0 Mm control areas for mapped files, FMsl 732490 140638080 0 0 STREAM_LIST_CTRL structure , Binary: Binary: nt!mm fltmgr.sys CcSc 104420 56804480 0 0 Cache Manager Shared Cache Map, Binary: SQSF 283749 45409984 0 0 UNKNOWN pooltag 'SQSF', please update nt!cc pooltag.txt FMfz 382318 42819616 0 0 FILE_LIST_CTRL structure, Binary: FMsc 0 0 SECTION_CONTEXT structure, Binary: fltmgr.sys 36130 32950560 fltmgr.sys EtwB 517 31297568 107 105119744 Etw Buffer , Binary: nt!etw DFmF 382318 30585440 382318 91756320 UNKNOWN pooltag 'DFmF', please update pooltag.txt DFmE 382318 18351264 0 0 UNKNOWN pooltag 'DFmE', please update FSfc 382318 18351264 0 0 Unrecoginzed File System Run Time pooltag.txt allocations (update pooltag.w), Binary: nt!fsrtl smNp 4295 17592320 0 0 ReadyBoost store node pool allocations, Thre 5780 12837376 0 0 Thread objects , Binary: nt!ps Pool 8 12834368 0 0 Pool tables, etc. Binary: nt!store or rdyboost.sys Experiment: Fehlerbehebung bei einem Poolleck In diesem Experiment spüren Sie ein Leck im ausgelagerten Pool Ihres Systems mithilfe der in diesem Abschnitt beschriebenen Techniken auf und reparieren es. Um das Leck zu verursachen, verwenden Sie das Sysinternals-Tool Notmyfault. Gehen Sie dazu wie folgt vor: 1. Führen Sie die passende Bitversion von Notmyfault.exe für Ihr System aus. 2. Notmyfault.exe lädt den Gerätetreiber Myfault.sys und zeigt das Dialogfeld Not My Fault an, wobei die Registerkarte Crash ausgewählt ist. Klicken Sie stattdessen auf die Registerkarte Leak. Dabei sehen Sie jetzt Folgendes: → Kernelmodusheaps (Systemspeicherpools) Kapitel 5 375
3. Vergewissern Sie sich, dass die Einstellung von Leak/Second den Wert 1000 KB hat. 4. Klicken Sie auf Leak Paged. Dadurch sendet Notmyfault so lange Anforderungen zur Zuweisung aus dem ausgelagerten Pool an Myfault, bis Sie auf Stop Paged klicken. Normalerweise wird der ausgelagerte Pool auch dann nicht freigegeben, wenn Sie das Programm beenden, das seine Entstehung ausgelöst hat (durch Interaktion mit einem fehlerhaften Gerätetreiber). Der Pool bleibt leck, bis Sie das System neu starten. Um den Testvorgang zu vereinfachen, erkennt Myfault jedoch, dass der Prozess geschlossen wurde, und gibt seine Zuweisungen frei. 5. Öffnen Sie den Task-Manager, während der Pool leckt, rufen Sie die Registerkarte Leistung auf und klicken Sie auf Arbeitsspeicher. Sie können jetzt beobachten, dass der Wert von Ausgelagerter Pool ansteigt. Das können Sie auch in der Anzeige System Information von Process Explorer erkennen. Öffnen Sie dazu das Menü View, wählen Sie System Information und klicken Sie auf die Registerkarte Memory. 6. Um zu ermitteln, welcher Pool leckt, führen Sie Poolmon aus und drücken (B), um die Ausgabe nach der Anzahl der Bytes zu sortieren. 7. Drücken Sie zweimal (P), damit Poolmon nur den ausgelagerten Pool anzeigt. Das Tag Leak klettert an die Spitze der Liste. Poolmon hebt geänderte Zeilen hervor, um Sie auf Änderungen bei den Poolzuweisungen hinzuweisen. 8. Klicken Sie auf Stop Paged, damit Sie den ausgelagerten Pool auf Ihrem System nicht erschöpfen. 9. Führen Sie das Sysinternals-Tool Strings aus, um nach den Binärdateien der Treiber zu suchen, die das Pooltag Leak enthalten. Als Ergebnis wird die Datei Myfault.sys angezeigt, womit bestätigt ist, dass dieser Treiber das Leck hervorruft. Strings %SystemRoot%\system32\drivers\*.sys | findstr Leak 376 Kapitel 5 Speicherverwaltung
Look-Aside-Listen Mit den Look-Aside-Lists bietet Windows einen Mechanismus zur schnellen Speicherzuweisung. Der grundlegende Unterschied zwischen Pools und Look-Aside-Listen besteht darin, dass Poolzuweisungen unterschiedliche Größen aufweisen können, wohingegen Look-Aside-Listen nur Blöcke fester Größe enthalten. Pools bieten zwar mehr Flexibilität, doch Look-Aside-Listen sind schneller, da sie keine Spinlocks verwenden. Exekutivkomponenten und Gerätetreiber können Look-Aside-Listen mit passenden Blockgrößen für häufig zugewiesene Datenstrukturen erstellen, indem sie die im WDK dokumentierte Funktion ExInitializeNPagedLookasideList (für nicht ausgelagerte Zuweisungen) bzw. ExInitializePagedLookasideList (für auslagerungsfähige Zuweisungen) verwenden. Um den Mehraufwand für die Prozessorsynchronisierung zu vermeiden, können mehrere Exekutivteilsysteme wie der E/A-Manager, der Cache-Manager und der Objekt-Manager für die einzelnen Prozesse separate Look-Aside-Listen für die jeweils besonders häufig verwendeten Datenstrukturen anlegen. Die Exekutive erstellt auch eine allgemeine prozessorweise Look-Aside-Liste für kleine Zuweisungen (bis zu 256) unabhängig davon, ob diese Zuweisungen ausgelagert werden können oder nicht. Wenn eine Look-Aside-Liste leer ist (was immer der Fall ist, wenn sie neu erstellt wurde), muss das System Zuweisungen aus dem ausgelagerten oder nicht ausgelagerten Pool vornehmen. Enthält die Liste einen freigegebenen Block, kann die Zuweisung jedoch sehr schnell erfüllt werden. Die Liste wächst dadurch, dass ihr Blöcke zurückgegeben werden. Die Routinen zur Poolzuweisung passen die Anzahl der freigegebenen Blöcke, die in den Look-Aside-Listen gespeichert sind, automatisch daran an, wie oft ein Gerätetreiber oder ein Exekutivteilsystem Zuweisungen aus der Liste vornimmt: Je häufiger die Zuweisungen geschehen, umso mehr Blöcke werden in der Liste gespeichert. Erfolgen keine Zuweisungen aus einer Liste, so wird sie automatisch verkleinert. Diese Überprüfung wird einmal pro Sekunde durchgeführt, wenn der Systemthread des Balance-Set-Managers aufwacht und die Funktion ExAdjustLookasideDepth aufruft. Experiment: Die Look-Aside-Listen des Systems einsehen Inhalt und Größe der einzelnen Look-Aside-Listen des Systems können Sie sich mit dem Kerneldebuggerbefehl !lookaside anzeigen lassen. Das folgende Beispiel zeigt einen Auszug aus der Ausgabe dieses Befehls: lkd> !lookaside Lookaside "nt!CcTwilightLookasideList" @ 0xfffff800c6f54300 Tag(hex): 0x6b576343 "CcWk" Type = 0200 NonPagedPoolNx Current Depth = 0 Max Depth = 4 Size = 128 Max Alloc = 512 AllocateMisses = 728323 FreeMisses = 728271 → Kernelmodusheaps (Systemspeicherpools) Kapitel 5 377
TotalAllocates = 1030842 Hit Rate = TotalFrees = 1030766 29% Hit Rate = 29% Lookaside "nt!IopSmallIrpLookasideList" @ 0xfffff800c6f54500 Tag(hex): 0x73707249 "Irps" Type = 0200 Current Depth = 0 Max Depth = 4 Size = 280 Max Alloc = 1120 AllocateMisses = 44683 FreeMisses = 43576 TotalAllocates = 232027 Hit Rate = NonPagedPoolNx TotalFrees = 230903 80% Hit Rate = 81% Lookaside "nt!IopLargeIrpLookasideList" @ 0xfffff800c6f54600 Tag(hex): 0x6c707249 "Irpl" Type = 0200 Current Depth = 0 NonPagedPoolNx Max Depth = 4 Size = 1216 Max Alloc = 4864 AllocateMisses = 143708 FreeMisses = 142551 TotalAllocates = 317297 TotalFrees = 316131 Hit Rate = 54% Hit Rate = 54% ... Total NonPaged currently allocated for above lists = 0 Total NonPaged potential for above lists = 13232 Total Paged currently allocated for above lists = 0 Total Paged potential for above lists = 4176 Der Heap-Manager Die meisten Anwendungen müssen Blöcke zuweisen, die kleiner als das bei der Verwendung von Funktionen mit Seitengranularität wie VirtualAlloc mögliche Minimum von 64 KB sind. Die Zuweisung eines so großen Bereichs für einen relativ kleinen Block ist für die Ausnutzung des Speichers und die Leistung alles andere als optimal. Um dieses Problem zu lösen, gibt es in Windows den Heap-Manager, der sich um Zuweisungen innerhalb der von Funktionen mit Seitengranularität reservierten größeren Speicherbereiche kümmert. Die Zuweisungsgranularität des Speicher-Managers ist relativ klein: 8 Bytes auf 32- und 16 Bytes auf 64-Bit-Systemen. Der Heap-Manager befindet sich in Ntdll.dll und in Ntoskrnl.exe. Die Teilsystem-APIs (wie die Windows-Heap-APIs) rufen die Funktionen in Ntdll.dll auf, verschiedene Exekutivkomponenten und Gerätetreiber dagegen verwenden die Funktionen aus Ntoskrnl.exe. Die nativen Schnittstellen des Heap-Managers (mit dem Präfix Rtl) stehen nur für interne Windows-Komponenten und Kernelmodus-Gerätetreiber zur Verfügung. Die dokumentierten Windows-API-Schnittstellen für den Heap (mit dem Präfix Heap) sind Forwarder zu den nativen Funktionen in Ntdll.dll. Darüber hinaus werden ältere APIs (mit dem Präfix Local oder Global) zur Unterstützung älterer 378 Kapitel 5 Speicherverwaltung
Windows-Anwendungen bereitgestellt. Auch sie rufen intern den Heap-Manager auf, wobei sie einige seiner besonderen Schnittstellen für ältere Verhaltensweisen nutzen. Zu den am häufigsten verwendeten Windows-Heapfunktionen gehören die folgenden: QQ HeapCreate und HeapDestroy Sie erstellen bzw. entfernen einen Heap. Beim Erstellen kann die ursprünglich reservierte und zugesicherte Größe angegeben werden. QQ HeapAlloc Diese Funktion weist einen Heapblock zu und wird zu RtlAllocateHeap in Ntdll.dll weitergeleitet. QQ HeapFree Gibt einen zuvor mit HeapAlloc zugewiesenen Block frei. QQ HeapReAlloc Ändert die Größe einer bestehenden Zuweisung; das heißt, der vorhandene Block wird vergrößert oder verkleinert. Diese Funktion wird zu RtlReAllocateHeap in Ntdll.dll weitergeleitet. QQ HeapLock und HeapUnlock seitig ausschließen. QQ HeapWalk Diese Funktion steuern Heapoperationen, die sich gegen- Zählt sämtliche Einträge und Regionen in einem Heap auf. Prozessheaps Jeder Prozess verfügt über mindestens einen Heap, nämlich seinen Standardprozessheap, der beim Prozessstart erstellt und während der Lebensdauer des Prozesses niemals gelöscht wird. Die Standardgröße beträgt 1 MB, aber Sie können ihn auch größer machen, indem Sie in der Abbilddatei mit dem Linkerflag /HEAP eine Anfangsgröße angeben. Bei diesem Wert handelt es sich jedoch lediglich um die ursprünglich reservierte Größe. Der Heap wird automatisch nach Bedarf erweitert. In der Abbilddatei können Sie auch eine ursprünglich zugesicherte Größe angeben. Der Standardheap kann ausdrücklich von einem Programm und stillschweigend durch einige interne Windows-Funktionen verwendet werden. Um den Standardprozessheap abzufragen, kann eine Anwendung die Windows-Funktion GetProcessHeap aufrufen. Mit HeapCreate können Prozesse auch zusätzliche private Heaps erstellen. Wenn ein Prozess einen privaten Heap nicht mehr benötigt, kann er den virtuellen Adressraum zurückgewinnen, indem er HeapDestroy aufruft. In jedem Prozess wird ein Array mit allen Heaps unterhalten. Threads können diese Heaps mit der Windows-Funktion GetProcessHeaps abfragen. Ein UWP-Prozess (Universal Windows Platform) schließt mindestens drei Heaps ein: QQ Den Standardprozessheap. QQ Einen gemeinsam genutzten Heap, um umfangreiche Argumente an die Csrss.exe-Instanz der Prozesssitzung zu übergeben. Er wird von der Ntdll.dll-Funktion CsrClientConnectToServer erstellt, die zu einem frühen Zeitraum der Prozessinitialisierung durch Ntdll.dll ausgeführt wird. Das Heaphandle steht in der globalen Variablen CsrPortHeap (in Ntdll.dll) zur Verfügung. QQ Ein von der C-Laufzeitbibliothek von Microsoft erstellter Heap. Sein Handle wird in der globalen Variablen _crtheap im Modul msvcrt gespeichert. Dies ist der Heap, der intern von den C/C++-Speicherzuweisungsfunktionen wie malloc, free, operator new/delete usw. verwendet wird. Der Heap-Manager Kapitel 5 379
QQ Ein Heap kann Zuweisungen in großen Speicherregionen vornehmen, die über VirtualAlloc vom Speicher-Manager reserviert wurden, aber auch in Dateiobjekten, die im Prozess­ adressraum zugeordnet wurden. Letzteres wird nur selten verwendet, und die WindowsAPI bietet auch keine Möglichkeit dafür. Allerdings ist diese Vorgehensweise für Situationen geeignet, in denen der Inhalt der Blöcke von zwei Prozessen oder von einer Kernel- und einer Benutzermoduskomponente gemeinsam genutzt werden muss. Der Win32-GUI-Teilsystemtreiber (Win32k.sys) verwendet einen solchen Heap, um GDI- und USER-Objekte mit dem Benutzermodus zu teilen. Ein Heap, der auf der Region einer im Speicher zugeordneten Datei aufbaut, unterliegt gewissen Einschränkungen: QQ Die internen Heapstrukturen verwenden Zeiger, weshalb eine Neuzuordnung zu anderen Adressen in anderen Prozessen nicht erlaubt ist. QQ Die Heapfunktionen ermöglichen keine Synchronisierung zwischen mehreren Prozessoren oder zwischen einer Kernelkomponente und einem Benutzerprozess. QQ Wird der Heap vom Benutzer- und vom Kernelmodus gemeinsam genutzt, sollte die Benutzermoduszuordnung schreibgeschützt sein, um zu verhindern, dass Benutzermoduscode die internen Strukturen des Heaps beschädigt, was das System zum Absturz bringen würde. Der Kernelmodustreiber muss außerdem darauf achten, dass keine sensiblen Daten auf einen gemeinsam genutzten Heap gelegt werden, damit sie nicht in den Benutzermodus durchsickern können. Arten von Heaps Bis zu Windows 10 und Windows Server 2016 gab es nur einen Typ von Heap, nämlich den NT-Heap. Er kann durch eine optionale Front-End-Schicht erweitert werden, die aus dem Low-Fragmentation-Heap (LFH) besteht. In Windows 10 wurde Segmentheaps eingeführt. Die beiden Heaptypen bestehen aus den gleichen Elementen, sind aber unterschiedlich aufgebaut und implementiert. Standardmäßig werden Segmentheaps von allen UWP-Apps und einigen Systemprozessen verwendet, NT-­ Heaps dagegen von allen übrigen Prozessen. Wie im Abschnitt »Segmentheaps« weiter hinten gezeigt, können Sie dies in der Registrierung ändern. NT-Heaps Wie Sie in Abb. 5–8 sehen, bestehen NT-Heaps im Benutzermodus aus zwei Schichten, nämlich einem Front-End und einem Back-End (wobei Letzteres auch als Heapkern bezeichnet wird). Das Back-End kümmert sich um die grundlegende Funktionalität wie die Verwaltung der Blöcke in den Segmenten, die Verwaltung der Segmente, die Richtlinien für die Erweiterung des Heaps, Zusicherung von Arbeitsspeicher und Aufheben der Zusicherung sowie die Verwaltung großer Blöcke. 380 Kapitel 5 Speicherverwaltung
Anwendung Heap-APIs Kernel32.Dll Front-End-Schicht des Heaps NtDll.Dll Back-End des Heaps Benutzermodus Kernelmodus Speicher-Manager Abbildung 5–8 Aufbau von NT-Heaps im Benutzermodus Nur bei Heaps im Benutzermodus kann es über dieser Kernfunktionalität noch eine Front-EndSchicht geben. Windows unterstützt eine einzige dieser optionalen Schichten, die LFH, die in dem Abschnitt »Der Low-Fragmentation-Heap« weiter hinten beschrieben wird. Heapsynchronisierung Der Heap-Manager unterstützt standardmäßig den gleichzeitigen Zugriff durch mehrere Threads. Ein Prozess, der nur einen einzigen Thread hat oder einen externen Mechanismus zur Synchronisierung verwendet, kann den Heap-Manager jedoch anweisen, auf den Mehraufwand für die Synchronisierung zu verzichten, indem er beim Erstellen des Heaps oder für einzelne Zuweisungen das Flag HEAP_NO_SERIALIZE angibt. Bei aktivierter Heapsynchronisierung werden alle internen Heapstrukturen durch eine einzige Sperre pro Heap geschützt. Wenn ein Prozess Operationen ausführt, die einen über mehrere Heapaufrufe hinweg konsistenten Zustand erfordern, kann er auch den gesamten Heap sperren, sodass andere Threads keine Heapoperationen durchführen können. Beispielsweise ist es für die Aufzählung der Blöcke in einem Heap mithilfe der Windows-Funktion HeapWalk erforderlich, den Heap zu sperren, wenn andere Threads gleichzeitig ebenfalls Operationen an ihm vornehmen könnten. Das Sperren und Entsperren eines Heaps erfolgt mit den Funktionen HeapLock und HeapUnlock. Der Low-Fragmentation-Heap Viele Anwendungen nutzen nur relativ wenig Heapspeicher, gewöhnlich weniger als 1 MB. Die »Passgenauigkeitsrichtlinie« des Heap-Managers hilft, den Speicherverbrauch für die Prozesse solcher Anwendungen gering zu halten. Für umfangreiche Prozesse und Mehrprozessorcomputer ist diese Vorgehensweise jedoch nicht geeignet, denn dabei kann der für die Heaps verfügbare Arbeitsspeicher aufgrund der Heapfragmentierung verringert werden. Wenn Threads, die auf verschiedenen Prozessoren laufen, ständig gleichzeitig Speicher der gleichen Größe verwenden, kann die Leistung leiden, da die Prozessoren gleichzeitig dieselben Stellen im Ar- Der Heap-Manager Kapitel 5 381
beitsspeicher bearbeiten müssen (z. B. den Anfang der Look-Aside-Liste für die betreffende Größe), wodurch eine starke Konkurrenz um die entsprechende Cachezeile entsteht. Der Low-Fragmentation-Heap verringert die Fragmentierung dadurch, dass er für Zuweisungen sogenannte Buckets verwendet. Dabei handelt es sich um vordefinierte Bereiche mit unterschiedlichen Blockgrößen. Wenn ein Prozess Speicher vom Heap zuweist, wählt der LFH den Bucket mit den kleinsten Blöcken aus, in die die gewünschte Zuweisung noch passt. Die kleinste Blockgröße beträgt 8 Bytes. Der erste Bucket wird daher für Zuweisungen zwischen 1 und 8 Bytes verwendet, der zweite für Zuweisungen zwischen 9 und 16 Bytes usw. bis zum 32. Bucket für Zuweisungen zwischen 249 und 256 Bytes. Der anschließende 33. Bucket ist für Zuweisungen zwischen 257 und 272 Bytes da usw. Der letzte Bucket ist der 128. und wird für Zuweisungen zwischen 15.873 und 16.384 Bytes verwendet. Bei Zuweisungen von mehr als 16.384 Bytes leitet der LFH die Anforderung einfach an das unter ihm liegende Back-End des Heaps weiter. Tabelle 5–7 gibt einen Überblick über die Buckets mit ihrer jeweiligen Granularität und den Größenbereichen, für die sie verwendet werden. Buckets Granularität Bereich 1–32 8 1–256 33–48 16 257–512 49–64 32 513–1024 65–80 64 1025–2048 81–96 128 2049–4096 97–112 256 4097–8192 113–128 512 8193–16.384 Tabelle 5–7 LFH-Buckets Um das zu erreichen, greift der LFH auf den Kern des Heap-Managers und auf Look-Aside-Listen zurück. Der Heap-Manager enthält einen automatischen Optimierungsmechanismus, der unter bestimmten Bedingungen automatisch den LFH verwendet, z. B. bei Konkurrenz um Sperren oder beim Vorhandensein von Zuweisungen häufig verwendeter Größen, die erfahrungsgemäß eine bessere Leistung zeigen, wenn der LFH eingeschaltet ist. Bei großen Heaps fällt ein erheblicher Anteil der Zuweisungen auf eine relativ kleine Anzahl von Buckets bestimmter Größen. Das vom LFH verwendete Verfahren dient dazu, Zuweisungen, die einem solchen Muster folgen, durch die effiziente Handhabung von Blöcken gleicher Größe zu optimieren. Um Skalierbarkeit zu erreichen, erweitert der LFH die internen Strukturen, auf die häufig zugegriffen wird, zu doppelt so vielen Slots, wie es zurzeit Prozessoren in dem Computer gibt. Die Zuweisung von Threads zu diesen Slots erfolgt durch eine LFH-Komponente, die als Affinitäts-Manager bezeichnet wird. Zu Anfang verwendet der LFH den ersten Slot für Heapzuweisungen, doch wenn er beim Zugriff auf interne Daten Konkurrenzbedingungen erkennt, schaltet er den aktuellen Thread zu einem anderen Slot um. Mehr Konkurrenzbedingungen führen dazu, dass die Threads auf mehr Slots verteilt werden. Für jede Bucketgröße gibt es eigene Slots, um die Lokalität zu verbessern und den Gesamtverbrauch an Arbeitsspeicher zu minimieren. 382 Kapitel 5 Speicherverwaltung
Auch wenn der LFH als Front-End eingeschaltet ist und sich um die Zuweisung der häufigsten Größenordnungen kümmert, können für die Zuweisungen seltener genutzter Größen nach wie vor die Heapkernfunktionen verwendet werden. Ist der LFH einmal für einen Heap aktiviert, so kann er nicht mehr deaktiviert werden. In Windows 7 und früheren Versionen war es mit der API HeapSetInformation und der Informationsklasse HeapCompatibilityInformation möglich, die LFH-Schicht wieder zu entfernen, aber in neueren Versionen wird sie ignoriert. Segmentheaps Abb. 5–9 zeigt den Aufbau der in Windows 10 eingeführten Segmentheaps. Anwendung Heap-APIs LFH <= 16.368 Bytes Kernel32.Dll Variable Größe (VS) <= 128 KB NtDll.Dll Back-End des Heaps (<= 508 KB) Zuweisungen großer Blöcke > 508 KB Benutzermodus Kernelmodus Speicher-Manager Abbildung 5–9 Segmentheaps Welche Schicht sich um eine Zuweisung kümmert, hängt von der Zuweisungsgröße ab: QQ Bei geringen Größen (kleiner oder gleich 16.368 Bytes) wird der LFH-Allozierer verwendet, die dem LFH-Front-End von NT-Heaps ähnelt. Dies gilt allerdings nur für häufig verwendete Größen. Anderenfalls wird der VS-Allozierer verwendet (Variable Size). QQ Bei Größen kleiner oder gleich 128 KB (die nicht vom LFH verwaltet werden) wird der VS-Allozierer benutzt. Sowohl der VS- als auch der LFH-Allozierer verwenden das BackEnd, um die erforderlichen Teilsegmente des Heaps nach Bedarf zu erstellen. QQ Zuweisungen größer als 128 KB und kleiner oder gleich 508 KB werden direkt vom HeapBack-End vorgenommen. QQ Zuweisungen größer als 508 KB werden durch direkten Aufruf des Speicher-Managers (VirtualAlloc) vorgenommen, da bei ihrer Größe die Standardgranularität von 64 KB (mit Rundung auf die nächste Seitengröße) als ausreichend angesehen wird. Der Heap-Manager Kapitel 5 383
Die beiden Heapimplementierungen unterscheiden sich wie folgt: QQ In manchen Situationen können Segmentheaps etwas langsamer sein als NT-Heaps. Sehr wahrscheinlich werden sie jedoch in zukünftigen Versionen von Windows mit den NT-­ Heaps gleichziehen. QQ Segmentheaps verbrauchen weniger Speicher für ihre Metadaten, weshalb sie sich für Geräte mit wenig Speicher besser eignen, z. B. für Telefone. QQ Bei Segmentheaps sind die Metadaten von den eigentlichen Daten getrennt, während sie bei NT-Heaps mit den Daten vermischt sind. Dadurch ist ein Segmentheap sicherer, da es bei ihm schwieriger ist, allein aufgrund der Blockadresse an die Metadaten einer Zuweisung zu gelangen. QQ Ein Segmentheap lässt sich nur für einen Heap verwenden, der wachsen kann, aber nicht für eine vom Benutzer bereitgestellte im Speicher zugeordnete Datei. Wird versucht, in letzterem Fall einen Segmentheap zu erstellen, wird stattdessen ein NT-Heap angelegt. QQ Beide Arten von Heaps unterstützen LFH-Zuweisungen, allerdings mit völlig unterschiedlicher interner Implementierung. Was Speicherverbrauch und Leistung angeht, ist die Implementierung in Segmentheaps effizienter. Wie bereits erwähnt, verwenden UWP-Anwendungen standardmäßig Segmentheaps, hauptsächlich, weil sie aufgrund ihrer geringen Speicheranforderungen besonders gut für Geräte mit wenig Speicher geeignet sind. Segmentheaps werden auch von einigen Systemprozessen wie Ccrss.exe, Lsass.exe, Runtimebroker.exe, Services.exe, Smss.exe und Svchost.exe verwendet. Für Desktop-Anwendungen sind Segmentheaps jedoch nicht die Standardheaps, da es bei bestehenden Anwendungen zu Kompatibilitätsproblemen kommen könnte. Es ist jedoch wahrscheinlich, dass sie in zukünftigen Versionen zum Standard werden. Um die Verwendung von Segmentheaps für eine bestimmte ausführbare Datei zu aktivieren oder zu deaktivieren, setzen Sie wie folgt den IFEO-DWORD-Wert FrontEndHeapDebugOptions: QQ Bit 2 (4) zur Deaktivierung der Verwendung von Segmentheaps QQ Bit 3 (8) zur Aktivierung der Verwendung von Segmentheaps Die Verwendung von Segmentheaps können Sie auch global aktivieren und deaktivieren, indem Sie den DWORD-Wert Enabled zum Registrierungsschlüssel HKLM\SYSTEM\CurrentControlSet\ Control\Session Manager\Segment Heap hinzufügen. Bei einem Wert von 0 wird die Verwendung von Segmentheaps deaktiviert, bei einem von 0 verschiedenen Wert wird sie aktiviert. Experiment: Grundlegende Informationen über Heaps einsehen In diesem Experiment untersuchen wir einige Heaps eines UWP-Prozesses. 1. Starten Sie den Taschenrechner von Windows 10. Um ihn zu finden, klicken Sie auf Start und geben Calculator ein. 2. In Windows 10 ist der Taschenrechner eine UWP-App (Calculator.exe). Starten Sie WinDbg und verbinden Sie den Debugger mit dem Rechnerprozess. → 384 Kapitel 5 Speicherverwaltung
3. WinDbg klinkt sich in den Prozess ein. Geben Sie den Befehl !heap ein, um sich einen Überblick über die Heaps in dem Prozess zu verschaffen: 0:033> !heap Heap Address NT/Segment Heap 2531eb90000 Segment Heap 2531e980000 NT Heap 2531eb10000 Segment Heap 25320a40000 Segment Heap 253215a0000 Segment Heap 253214f0000 Segment Heap 2531eb70000 Segment Heap 25326920000 Segment Heap 253215d0000 NT Heap 4. Die einzelnen Heaps werden mit ihrem Handle und Typ (Segment- oder NT-Heap) angezeigt. Der erste Heap ist der Standardheap des Prozesses. Da er wachsen kann und keinen zuvor vorhandenen Speicherblock verwendet, wird er als Segmentheap erstellt. Der zweite Heap dagegen verwendet einen vom Benutzer definierten Speicherblock (siehe die Beschreibung im Abschnitt »Prozessheaps« weiter vorn), was zurzeit nicht von Segmentheaps unterstützt wird. Daher wird er als NT-Heap angelegt. 5. Ein NT-Heap wird von der NtDll!_HEAP-Struktur verwaltet. Schauen Sie sich diese Struktur für den zweiten Heap an: 0:033> dt ntdll!_heap 2531e980000 +0x000 Segment : _HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY +0x010 SegmentSignature : 0xffeeffee +0x014 SegmentFlags : 1 +0x018 SegmentListEntry : _LIST_ENTRY [ 0x00000253'1e980120 0x00000253'1e980120 ] +0x028 Heap : 0x00000253'1e980000 _HEAP +0x030 BaseAddress : 0x00000253'1e980000 Void +0x038 NumberOfPages : 0x10 +0x040 FirstEntry : 0x00000253'1e980720 _HEAP_ENTRY +0x048 LastValidEntry : 0x00000253'1e990000 _HEAP_ENTRY +0x050 NumberOfUnCommittedPages : 0xf +0x054 NumberOfUnCommittedRanges : 1 +0x058 SegmentAllocatorBackTraceIndex : 0 +0x05a Reserved : 0 +0x060 UCRSegmentList : _LIST_ENTRY [ 0x00000253'1e980fe0 - 0x00000253'1e980fe0 ] +0x070 Flags : 0x8000 → Der Heap-Manager Kapitel 5 385
+0x074 ForceFlags : 0 +0x078 CompatibilityFlags : 0 +0x07c EncodeFlagMask : 0x100000 +0x080 Encoding : _HEAP_ENTRY +0x090 Interceptor : 0 +0x094 VirtualMemoryThreshold : 0xff00 +0x098 Signature : 0xeeffeeff +0x0a0 SegmentReserve : 0x100000 +0x0a8 SegmentCommit : 0x2000 +0x0b0 DeCommitFreeBlockThreshold : 0x100 +0x0b8 DeCommitTotalFreeThreshold : 0x1000 +0x0c0 TotalFreeSize : 0x8a +0x0c8 MaximumAllocationSize : 0x00007fff'fffdefff +0x0d0 ProcessHeapsListIndex : 2 ... +0x178 FrontEndHeap : (null) +0x180 FrontHeapLockCount : 0 +0x182 FrontEndHeapType : 0 '' +0x183 RequestedFrontEndHeapType : 0 '' +0x188 FrontEndHeapUsageData : (null) +0x190 FrontEndHeapMaximumIndex : 0 +0x192 FrontEndHeapStatusBitmap : [129] "" +0x218 Counters : _HEAP_COUNTERS +0x290 TuningParameters : _HEAP_TUNING_PARAMETERS 6. Das Feld FrontEndHeap gibt an, ob eine Front-End-Schicht vorhanden ist. In der Beispielausgabe ist dieses Feld null, was bedeutet, dass es diese Schicht hier nicht gibt. Jeder andere Wert weist auf eine LFH-Front-End-Schicht hin, weil dies die einzige definierte Front-End-Schicht ist. 7. Ein Segmentheap wird durch eine Ntdll!_SEGMENT_HEAP-Struktur definiert. Sehen Sie sich diese Struktur für den Standardheap des Prozesses an: 0:033> dt ntdll!_segment_heap 2531eb90000 +0x000 TotalReservedPages : 0x815 +0x008 TotalCommittedPages : 0x6ac +0x010 Signature : 0xddeeddee +0x014 GlobalFlags : 0 +0x018 FreeCommittedPages : 0 +0x020 Interceptor : 0 +0x024 ProcessHeapListIndex : 1 +0x026 GlobalLockCount : 0 +0x028 GlobalLockOwner : 0 → 386 Kapitel 5 Speicherverwaltung
+0x030 LargeMetadataLock : _RTL_SRWLOCK +0x038 LargeAllocMetadata : _RTL_RB_TREE +0x048 LargeReservedPages : 0 +0x050 LargeCommittedPages : 0 +0x058 SegmentAllocatorLock : _RTL_SRWLOCK +0x060 SegmentListHead : _LIST_ENTRY [ 0x00000253'1ec00000 - 0x00000253'28a00000 ] +0x070 SegmentCount : 8 +0x078 FreePageRanges : _RTL_RB_TREE +0x088 StackTraceInitVar : _RTL_RUN_ONCE +0x090 ContextExtendLock : _RTL_SRWLOCK +0x098 AllocatedBase : 0x00000253'1eb93200 "" +0x0a0 UncommittedBase : 0x00000253'1eb94000 "--- memory read error at address 0x00000253'1eb94000 ---" +0x0a8 ReservedLimit : 0x00000253'1eba5000 "--- memory read error at address 0x00000253'1eba5000 ---" +0x0b0 VsContext : _HEAP_VS_CONTEXT +0x120 LfhContext : _HEAP_LFH_CONTEXT 8. Mit dem Feld Signature wird zwischen den beiden Arten von Heaps unterschieden. 9. In der Struktur für den NT-Heap befand sich am selben Offset (0x010) das Feld Segment­ Signature. Dadurch können Funktionen wie RtlAllocateHeap allein am Heaphandle (an der Adresse) erkennen, welche der beiden Implementierungen sie einschalten müssen. 10. Die letzten beiden Felder der _SEGMENT_HEAP-Struktur enthalten Angaben zum VS- und zum LFH-Allozierer. 11. Um weitere Informationen über die einzelnen Heaps zu gewinnen, verwenden Sie den Befehl !heap -s: 0:033> !heap -s Process Global Heap Address Signature Flags Total Total Heap Reserved Committed List Bytes Bytes Index (K) (K) 2531eb90000 ddeeddee 0 1 8276 6832 2531eb10000 ddeeddee 0 3 1108 868 25320a40000 ddeeddee 0 4 1108 16 253215a0000 ddeeddee 0 5 1108 20 253214f0000 ddeeddee 0 6 3156 816 2531eb70000 ddeeddee 0 7 1108 24 25326920000 ddeeddee 0 8 1108 32 → Der Heap-Manager Kapitel 5 387
**************************************************************************** ************* NT HEAP STATS BELOW **************************************************************************** ************* LFH Key : 0xd7b666e8f56a4b98 Termination on corruption : ENABLED Affinity manager status: - Virtual affinity limit 8 - Current entries in use 0 - Statistics: Swaps=0, Resets=0, Allocs=0 Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast (k) (k) (k) (k) length blocks cont. heap -----------------------------------------------------------------------------------000002531e980000 00008000 64 4 64 2 1 1 0 0 00000253215d0000 00000001 16 16 16 10 1 1 0 N/A ------------------------------------------------------------------------------------ 12. Der erste Teil der Ausgabe zeigt ausführliche Informationen über die Segmentheaps im Prozess (falls vorhanden), der zweite über die NT-Heaps. Der Debuggerbefehl !heap bietet viele Optionen zur Anzeige und Untersuchung von H ­ eaps. Mehr darüber erfahren Sie in der Dokumentation zu den Debugger-Tools für Windows. Sicherheitseinrichtungen von Heaps Im Laufe seiner Entwicklung hat der Heap-Manager eine immer größere Rolle bei der frühzeitigen Erkennung von Heapnutzungsfehlern und bei der Eindämmung der Auswirkungen von Heapexploits übernommen. Sowohl NT- als auch Segmentheaps verfügen über Mechanismen, um die Wahrscheinlichkeit einer Einflussnahme auf den Speicher zu verringern. Die von den Heaps zur internen Verwaltung verwendeten Metadaten werden mit einer starken Randomisierung gepackt, sodass es Angreifern schwerer fällt, die internen Strukturen zu manipulieren, um einen Absturz zu vermeiden oder einen Angriff zu verschleiern. Die Header der entsprechenden Blöcke unterliegen auch einer Integritätsprüfung, um einfache Beschädigungen wie Pufferüberläufe zu finden. Des Weiteren nutzt der Heap auch eine gewisse Randomisierung bei den Basisadressen und Handles. Durch die Verwendung der API HeapSetInformation mit der Klasse HeapEnableTerminationOnCorruption können Prozesse eine automatische Beendigung verlangen, wenn Inkonsistenzen festgestellt werden, sodass kein unbekannter Code ausgeführt wird. Die Randomisierung der Blockmetadaten führt jedoch auch dazu, dass es nicht viel bringt, mit einem Debugger einen Blockheader als Speicherbereich auszugeben. Beispielsweise lassen 388 Kapitel 5 Speicherverwaltung
sich die Größe des Blocks und die Angabe, ob er beschäftigt ist oder nicht, in der regulären Ausgabe nicht so leicht erkennen. Das Gleiche gilt auch für LFH-Blöcke. Bei ihnen sind im Header andere Arten von Metadaten gespeichert, die ebenfalls teilweise randomisiert sind. Um diese Informationen auszugeben, ruft der Debuggerbefehl !heap -i alle Metadatenfelder eines Blocks ab und macht ggf. auf vorhandene Inkonsistenzen bei der Prüfsumme und bei den Listen der freien Blöcke aufmerksam. Dieser Befehl funktioniert sowohl für LFH- als auch für reguläre Heapblöcke. Die Gesamtgröße der Blöcke, die vom Benutzer angeforderte Größe, das Segment, das den Block besitzt, und die Teilprüfsumme des Headers stehen wie im folgenden Beispiel gezeigt in der Ausgabe zur Verfügung. Da der Randomisierungsalgorithmus die Heapgranularität verwendet, sollte der Befehl !heap -i nur im Kontext des Heaps angewendet werden, der den Block enthält. In dem folgenden Beispiel lautet das Heaphandle 0x001a0000. Bei einem anderen Heapkontext würde der Header falsch decodiert. Um den richtigen Kontext festzulegen, müssen Sie den Befehl !heap -i mit dem Heaphandle als Argument vorab schon einmal ausführen. 0:004> !heap -i 000001f72a5e0000 Heap context set to the heap 0x000001f72a5e0000 0:004> !heap -i 000001f72a5eb180 Detailed information for block entry 000001f72a5eb180 Assumed heap : 0x000001f72a5e0000 (Use !heap -i NewHeapHandle to change) Header content : 0x2FB544DC 0x1000021F (decoded : 0x7F01007E 0x10000048) Owning segment : 0x000001f72a5e0000 (offset 0) Block flags : 0x1 (busy ) Total block size : 0x7e units (0x7e0 bytes) Requested size : 0x7d0 bytes (unused 0x10 bytes) Previous block size: 0x48 units (0x480 bytes) Block CRC : OK - 0x7f Previous block : 0x000001f72a5ead00 Next block : 0x000001f72a5eb960 Besondere Sicherheitseinrichtungen von Segmentheaps Segmentheaps nutzen viele Sicherheitsmechanismen, die es schwerer machen, den Speicher zu beschädigen oder Code zu injizieren, darunter die folgenden: QQ Sofortige Beendigung bei beschädigten Listenknoten Segmentheaps verwenden verknüpfte Listen, um den Überblick über Segmente und Teilsegmente zu wahren. Wie bei NT-Heaps werden beim Hinzufügen und Entfernen von Listenknoten Überprüfungen durchgeführt, um willkürliche Schreibvorgänge im Arbeitsspeicher aufgrund von beschädigten Listenknoten zu verhindern. Wird ein beschädigter Knoten entdeckt, wird der Prozess mit einem Aufruf von RtlFailFast beendet. QQ Sofortige Beendigung bei Schädigung von Schwarz-Rot-Baumknoten Segment­ heaps verwenden Schwarz-Rot-Bäume, um den Überblick über Back-End- und VS-Zuweisungen zu wahren. Die Funktionen zum Hinzufügen und Entfernen von Knoten führen eine Der Heap-Manager Kapitel 5 389
Validierung durch und rufen den Mechanismus für die sofortige Beendigung auf, wenn sie beschädigte Knoten feststellen. QQ Funktionszeigerdecodierung Bei einigen Aspekten von Segmentheaps sind Callbacks zulässig (in den Strukturen VsContext und LfhContext, die zur _SEGMENT_HEAP-Struktur gehören). Ein Angreifer kann diese Callbacks überschreiben, sodass sie auf seinen eigenen Code zeigen. Die Funktionszeiger werden jedoch mit einer XOR-Funktion mit einem internen, zufälligen Heapschlüssel und der Kontextadresse codiert, die sich beide nicht vorab raten lassen. QQ Wächterseiten Bei der Zuweisung von LFH- und VS-Teilsegmenten und großen Blöcken wird am Ende eine Wächterseite hinzugefügt. Sie hilft dabei, Überläufe und Beschädigungen angrenzender Daten zu erkennen. Weitere Informationen über Wächterseiten erhalten Sie im Abschnitt »Stacks« weiter hinten. Debugging einrichten für Heaps Der Heap-Manager enthält verschiedene Einrichtungen, um Bugs zu erkennen: QQ Blockendeüberprüfung Das Ende jedes Blocks enthält eine Signatur, die bei der Freigabe des Blocks überprüft wird. Wurde die Signatur durch einen Pufferüberlauf ganz oder teilweise zerstört, meldet der Heap diesen Fehler. QQ Überprüfung freier Blöcke Ein freier Block wird mit einem Muster gefüllt, das bei verschiedenen Gelegenheiten überprüft wird, bei denen der Heap-Manager auf den Block zugreifen muss, etwa wenn er ihn von der Liste der freien Blöcke entfernt, um eine Zuweisung durchzuführen. Wenn der Prozess nach der Freigabe eines Blocks weiterhin in diesen schreibt, erkennt der Heap-Manager die Änderungen an dem Muster und meldet den Fehler. QQ Parameterprüfung Hierbei erfolgt eine ausführliche Prüfung der an die Heapfunktionen übergebenen Parameter. QQ Heapvalidierung Bei jedem Heapaufruf wird der gesamte Heap validiert. Unterstützung für Heaptags und Stackablaufverfolgung Damit ist es möglich, Tags für die Zuweisung anzugeben bzw. eine Ablaufverfolgung des Benutzermodusstacks für Heapaufrufe zu erfassen, um die möglichen Ursachen von Heapfehlern einzuschränken. Die ersten drei Einrichtungen sind standardmäßig aktiviert, wenn der Lader erkennt, dass ein Prozess unter der Kontrolle eines Debuggers gestartet wurde (wobei der Debugger dieses Standardverhalten überschreiben und diese Einrichtungen ausschalten kann). Um die Heap­ debuggingeinrichtungen für ein ausführbares Abbild anzugeben, setzen Sie mit dem Tool Gflags verschiedene Debuggingflags im Abbildheader (siehe das nächste Experiment und den Abschnitt »Globale Windows-Flags« in Kapitel 8 von Band 2). Alternativ können Sie die Heapdebuggingoptionen auch mit dem Befehl !heap in Windows-Standarddebuggern einschalten (siehe die Hilfeseiten im Debugger). Die Aktivierung der Heapdebuggingoptionen gilt für alle Heaps in dem Prozess. Außerdem wird automatisch der LFH deaktiviert und der Kernheap verwendet, wenn auch nur eine dieser Optionen eingeschaltet ist. Der LFH wird auch nicht für Heaps verwendet, die nicht erweiter- 390 Kapitel 5 Speicherverwaltung
bar sind (aufgrund des Zusatzaufwands, der noch zu den vorhandenen Heapstrukturen hinzukommt), und ebenso nicht für Heaps, die keine Serialisierung erlauben. Der Seitenheap Da die Überprüfung des Blockendes und der freien Blöcke Beschädigungen aufdecken können, die lange zuvor erfolgt sind, gibt es mit dem Seitenheap noch eine zusätzliche Debuggingeinrichtung für Heaps. Er leitet einige oder alle Heapaufrufe an einen anderen Heapmanager weiter. Den Seitenheap können Sie mit Gflags aktivieren, das zu den Debugging-Tools für Windows gehört. Der Heap-Manager platziert Zuweisungen dann am Ende von Seiten und reserviert die unmittelbar folgende Seite. Da die reservierten Seiten nicht zugänglich sind, verursacht jeder Pufferüberlauf eine Zugriffsverletzung, was es einfacher macht, den Verursachercode zu finden. Optional können Blöcke auch am Anfang der Seiten platziert und die vorhergehende Seite reserviert werden, um Pufferunterläufe zu erkennen (die seltener vorkommen). Mit dem Seitenheap lassen sich auch freigegebene Seiten gegen jeglichen Zugriff schützen, um Verweise auf Heapblöcke nach deren Freigabe zu erkennen. Aufgrund der erheblichen Mehrbelegung selbst bei kleinen Zuweisungen kann die Verwendung des Seitenheaps bei 32-Bit-Prozessen dazu führen, dass Ihnen der Adressraum ausgeht. Durch die Zunahme an Verweisen auf Nullforderungsseiten, den Verlust der Lokalität und den Mehraufwand für häufige Aufrufe zur Validierung von Heapstrukturen kann außerdem die Leistung leiden. Ein Prozess kann diese Auswirkungen dadurch verringern, dass er festlegt, dass der Seitenheap nur für Blöcke bestimmter Größen, Adressbereiche oder Ursprungs-DLLs eingesetzt werden soll. Experiment: Den Seitenheap verwenden In diesem Experiment schalten Sie den Seitenheap für Notepad.exe ein und beobachten die Auswirkungen. 1. Starten Sie Notepad.exe. 2. Öffnen Sie den Task-Manager, rufen Sie die Registerkarte Details auf und fügen Sie die Spalte Zugesicherte Größe zur Anzeige hinzu. 3. Merken Sie sich die zugesicherte Größe der Notepad-Instanz. 4. Starten Sie Gflags.exe in dem Ordner mit den Debugging-Tools für Windows (erhöhte Rechte erforderlich). 5. Klicken Sie auf die Registerkarte Image File. 6. Geben Sie notepad.exe in das Textfeld Image ein und drücken Sie (Tab). → Der Heap-Manager Kapitel 5 391
7. Aktivieren Sie das Kontrollkästchen Enable Page Heap, sodass das Dialogfeld wie folgt aussieht: 8. Klicken Sie auf Übernehmen. 9. Starten Sie eine weitere Instanz von Notepad, ohne die erste zu schließen. 10. Vergleichen Sie im Task-Manager die zugesicherten Größen der beiden Notepad-In­ stanzen. Der Wert ist bei der zweiten Instanz viel größer, obwohl beide Notepad-Prozesse leer sind. Das liegt an den zusätzlichen Zuweisungen, die der Seitenheap vornimmt. Der folgende Screenshot zeigt diese Situation auf einem 32-Bit-Computer mit Windows 10: → 392 Kapitel 5 Speicherverwaltung
11. Um sich eine bessere Vorstellung von dem zusätzlich zugewiesenen Speicher zu machen, verwenden Sie das Sysinternals-Tool VMMap. Öffnen Sie VMMap.exe und markieren Sie die Notepad-Instanz, die den Seitenheap verwendet: 12. Öffnen Sie eine weitere Instanz von VMMap und markieren Sie darin die andere Notepad-Instanz. Ordnen Sie die beiden Fenster nebeneinander an, um beide überblicken zu können: Der Unterschied bei der zugesicherten Größe wird im Bereich Private Data (gelb) besonders deutlich. → Der Heap-Manager Kapitel 5 393
13. Klicken Sie in beiden VMMap-Fenstern jeweils im mittleren Teil der Anzeige auf die Zeile Private Data, um sich die Einzelheiten in der unteren Anzeige anzusehen. Im Screenshot sind die Daten nach Größe sortiert. 14. Der Prozess im linken Screenshot (Notepad mit Seitenheap) verbraucht deutlich mehr Speicher. Öffnen Sie einen der 1024-KB-Abschnitte. Dabei sehen Sie Folgendes: 15. Jetzt können Sie zwischen den zugesicherten Seiten deutlich die vom Seitenheap reservierten Seiten sehen, die dazu dienen, Pufferüberläufe und -unterläufe abzufangen. Deaktivieren Sie die Option Enable Page Heap in Gflags und klicken Sie auf Übernehmen, damit später aufgerufene Instanzen des Editors wieder ohne Seitenheap gestartet werden. Weitere Informationen über den Seitenheap finden Sie in der Hilfedatei zu den Debugging-Tools für Windows. Der fehlertolerante Heap Microsoft hat die Beschädigung von Heapmetadaten als eine der häufigsten Ursachen von Anwendungsversagen erkannt. Um dieses Problem einzudämmen und Anwendungsentwicklern bessere Möglichkeiten zur Problembehebung zur Verfügung zu stellen, gibt es in Windows den fehlertoleranten Heap (FTH). Er besteht aus den beiden folgenden Hauptkomponenten: QQ Die Erkennungskomponente (FTH-Server) QQ Die Eindämmungskomponente (FTH-Client) Bei der Erkennungskomponente handelt es sich um die DLL Fthsvc.dll, die vom Dienst des Windows-Sicherheitscenters (Wscsvc.dll) geladen wird. Dieser Dienst wiederum läuft in einem der gemeinsam genutzten Dienstprozesse unter dem lokalen Dienstkonto und wird vom Windows-Fehlerberichterstattungsdienst (Windows Error Reporting, WER) über Abstürze von Anwendungen informiert. Nehmen wir an, eine Anwendung stürzt in Ntdll.dll mit einem Fehlerstatus ab, der auf eine Zugriffsverletzung oder eine Ausnahme aufgrund einer Heapbeschädigung hindeutet. 394 Kapitel 5 Speicherverwaltung
Wenn sich diese Anwendung noch nicht auf der Überwachungsliste des FTH-Dienstes befindet, erstellt dieser für sie ein »Ticket«, um die FTH-Daten festzuhalten. Stürzt die Anwendung anschließend mehr als vier Mal in einer Stunde ab, richtet der FTH-Dienst sie so ein, dass sie in Zukunft den FTH-Client verwendet. Der FTH-Client ist ein Anwendungskompatibilitätsshim. Dieser Mechanismus wird seit Windows XP verwendet, damit Anwendungen, die auf ein bestimmtes Verhalten älterer Windows-Systeme angewiesen sind, auch auf moderneren Systemen ausgeführt werden können. In diesem Fall fängt der Shimmechanismus Aufrufe an die Heaproutinen ab und leitet sie zu seinem eigenen Code weiter. Dieser FTH-Code implementiert verschiedene Abhilfemaßnahmen, die es der Anwendung erlauben, trotz der Heapfehler weiterzumachen. Beispielsweise fügt FTH als Schutz gegen kleine Pufferüberlauffehler allen Zuweisungen eine Aufpolsterung von 8 Bytes sowie einen reservierten FTH-Bereich hinzu. Um das häufig auftretende Problem zu lösen, dass ein Zugriff auf einen bereits freigegebenen Heapblock erfolgt, werden HeapFree-Aufrufe erst nach einer gewissen Verzögerung umgesetzt. Die zur Freigabe vorgemerkten Blöcke werden auf eine Liste gesetzt und erst dann tatsächlich freigegeben, wenn die Gesamtgröße der Blöcke auf dieser Liste 4 MB übersteigt. Versuche, Regionen freizugeben, die nicht zu dem Heap oder zu dem Teil des Heaps gehören, der im Handleargument von HeapFree angegeben wurde, werden einfach ignoriert. Des Weiteren werden nach dem Aufruf von exit oder RtlExitUserProcess keine Blöcke mehr tatsächlich freigegeben. Der FTH-Server beobachtet die Ausfallrate der Anwendung nach der Einrichtung der Abhilfemaßnahmen. Verbessert sich die Ausfallrate nicht, werden die Abhilfemaßnahmen wieder entfernt. Die Aktivitäten des FTH können Sie in der Ereignisanzeige beobachten. Gehen Sie dazu wie folgt vor: 1. Öffnen Sie das Dialogfeld Ausführen und geben Sie eventvwr.msc ein. 2. Erweitern Sie im linken Bereich Ereignisanzeige > Anwendungs- und Dienstprotokolle > Microsoft > Windows > Fault-Tolerant-Heap. 3. Klicken Sie auf das Protokoll Betriebsbereit. 4. Wenn Sie den FTH komplett deaktivieren wollen, setzen Sie den Wert Enabled im Registrierungsschlüssel HKLM\Software\Microsoft\FTH auf 0. Dieser Registrierungsschlüssel enthält auch verschiedene FTH-Einstellungen, z. B. die bereits erwähnte Verzögerung und eine Ausnahmeliste von ausführbaren Dateien (auf der sich u. a. Standardsystemprozesse wie Smss.exe, Ccrss.exe, Wininit.exe, Services.exe, Winlogon.exe und Taskhost.exe befinden). Im Wert RuleList gibt es außerdem eine Regelliste, die die Module und Ausnahmetypen sowie einige Flags angibt, die überwacht werden müssen, um zu entscheiden, ob der FTH zugeschaltet werden soll oder nicht. Standardmäßig wird hier nur eine einzige Regel aufgeführt, um Heapprobleme vom Typ STATUS_ACCESS_VIOLATION (0xc0000005) in Ntdll. dll zu erkennen. Der FTH wird normalerweise nicht auf Dienste angewendet. In den Windows-Serversystemen ist er aus Leistungsgründen deaktiviert. Mit dem Application Compatibility Toolkit können Systemadministratoren das Shim manuell auf die ausführbare Datei einer Anwendung oder eines Dienstes anwenden. Der Heap-Manager Kapitel 5 395
Layouts für virtuelle Adressräume In diesem Abschnitt geht es um die Komponenten im Benutzer- und im Systemadressraum und die besonderen Layouts auf 32-Bit-Systemen (x86 und ARM) und 64-Bit-Systemen (x64). Dadurch wird deutlich, welche Einschränkungen auf diesen Plattformen jeweils für den virtuellen Prozess- und Systemarbeitsspeicher gelten. In Windows werden vor allem die drei folgenden Arten von Daten im virtuellen Adressraum zugeordnet: QQ Privater Code und private Daten der einzelnen Prozesse Wie bereits in Kapitel 1 erwähnt, kann jeder Prozess über einen privaten Adressraum verfügen, der für andere Prozesse nicht zugänglich ist. Diese virtuellen Adressen werden also stets im Kontext des aktuellen Prozesses ausgewertet und können nicht auf Adressen verweisen, die von anderen Prozessen definiert wurden. Threads innerhalb des Prozesses können daher niemals auf virtuelle Adressen außerhalb dieses privaten Adressraums zugreifen. Auch der gemeinsam genutzte Speicher bildet keine Ausnahme von dieser Regel, da die entsprechenden Speicherbereiche den einzelnen teilnehmenden Prozessen zugeordnet werden, sodass jeder Prozess mithilfe seiner prozesseigenen Adressen darauf zugreift. Auch prozessübergreifende Speicherfunktionen (ReadProcessMemory und WriteProcessMemory) führen Kernelmoduscode im Kontext des Zielprozesses aus. Der virtuelle Adressraum des Prozesses – die Seitentabellen – wird im Abschnitt »Adressübersetzung« beschrieben. Diese Tabellen werden auf Seiten gespeichert, die nur vom Kernelmodus aus zugänglich sind, weshalb die Benutzermodusthreads eines Prozesses nicht in der Lage sind, das Layout des eigenen Prozessraums zu ändern. QQ Code und Daten der Sitzung Der Sitzungsraum enthält Informationen, die für die gesamte Sitzung gelten (siehe Kapitel 2). Eine Sitzung besteht aus den Prozessen und anderen Systemobjekten, die eine einzelne Anmeldesitzung eines Benutzers ausmachen, zum Beispiel der Arbeitsstation, den Desktops und den Fenstern. Jede Sitzung verfügt über ihren eigenen Bereich im ausgelagerten Pool, der vom Kernelmodusteil des Windows-Teilsystems (Win32k.sys) für die Zuweisung privater GUI-Datenstrukturen der Sitzung verwendet wird. Außerdem hat jede Sitzung ihre eigene Kopie des Windows-Teilsystemprozesses (Csrss.exe) und des Anmeldeprozesses (Winlogon.exe). Der Prozess des Sitzungs-Managers (Smss.exe) ist dafür zuständig, neue Sitzungen zu erstellen. Dazu gehört es, die private Win32k.sys-Kopie der Sitzung zu laden, den privaten Objekt-Manager-Namensraum der Sitzung anzulegen (siehe Kapitel 8 von Band 2) und die sitzungsspezifischen Instanzen von Csrss.exe und Winlogon.exe zu erstellen. Zur Virtualisierung von Sitzungen werden alle sitzungsweiten Datenstrukturen in einer Region des Systemraums zugeordnet, die als Sitzungsraum bezeichnet wird. Beim Erstellen eines Prozesses wird dieser Adressbereich den Seiten der Sitzung zugeordnet, zu der der Prozess gehört. QQ Code und Daten des Systems Der Systemraum enthält globalen Betriebssystemcode und globale Datenstrukturen, die der Kernelmoduscode unabhängig davon sehen kann, welcher Prozess zurzeit ausgeführt wird. Er besteht aus den folgenden Komponenten: • 396 Systemcode Dies schließt das Betriebssystemabbild, die HAL und die Gerätetreiber zum Starten des Systems ein. Kapitel 5 Speicherverwaltung
• Nicht ausgelagerter Pool Dies ist der nicht auslagerungsfähige Systemspeicher­ heap. • Ausgelagerter Pool • Systemcache Dies ist der virtuelle Adressraum zur Zuordnung von Dateien, die im Systemcache geöffnet sind (siehe Kapitel 11 in Band 2). • Systemweite Seitentabelleneinträge (Page Entry Tables, PTEs) Dies ist der Pool der System-PTEs, die zur Zuordnung von Systemseiten wie dem E/A-Raum, Kernel­ stacks und Speicherdeskriptorlisten verwendet werden. Mit dem Leistungsindikator Arbeitsspeicher:Freie Seitentabelleneinträge in der Leistungsüberwachung können Sie sich ansehen, wie viele System-PTEs verfügbar sind. • Systemweite Arbeitssatzlisten Dies sind die Datenstrukturen für die Arbeitssatzlisten, die die drei Arbeitssätze Systemcache, ausgelagerter Pool und System-PETs beschreiben. • Zugeordnete Systemsichten Sie werden verwendet, um Win32k.sys – den ladbaren Kernelmodusteil des Windows-Teilsystems – sowie die davon verwendeten Kernelmodus-Grafiktreiber zuzuordnen. • Hyperspace Dies ist ein besonderer Bereich zur Zuordnung der Arbeitssatzliste und anderen prozessweisen Daten, die nicht in einem willkürlichen Prozesskontext zugänglich sein müssen. Hyperspace wird auch zur vorübergehenden Zuordnung von physischen Seiten im Systemraum verwendet. Ein Beispiel dafür ist die Aufhebung von Einträgen in den Seitentabellen von Prozessen außer dem aktuellen, z. B. wenn eine Seite von der Standbyliste entfernt wird. • Absturzabbildinformationen Dies ist zur Aufzeichnung von Informationen über den Status eines Systemabsturzes reserviert. • HAL-Nutzung Dies ist der auslagerungsfähige Systemspeicherheap. Dies ist der für HAL-spezifische Strukturen reservierte Systemspeicher. Nachdem Sie nun die Grundbestandteile des virtuellen Adressraums in Windows kennen, wollen wir uns die jeweiligen Layouts für die x86-, die ARM- und die x64-Plattform ansehen. x86-Adressraumlayouts In den 32-Bit-Versionen von Windows verfügt jeder Benutzerprozess standardmäßig über einen privaten Adressraum von 2 GB (wobei das Betriebssystem die restlichen 2 GB in Anspruch nimmt). Allerdings können x86-Systeme mit der BCD-Startoption increaseuserva mit Benutzeradressräumen von bis zu 3 GB eingerichtet werden. Die beiden möglichen Adressraumlayouts sehen Sie in Abb. 5–10. Diese Möglichkeit wurde eingeführt, um den Bedarf von 32-Bit-Anwendungen zu befriedigen, mehr Daten im Speicher zu halten, als es bei einem Adressraum von 2 GB möglich ist. 64-Bit-Systeme bieten natürlich einen weit umfangreicheren Adressraum. Layouts für virtuelle Adressräume Kapitel 5 397
Für HAL reserviert (4MB) Systemcache Ausgelagerter Pool Nicht ausgelagerter Pool 1 GB Systemraum Hyperspace (4 MB) Prozessseitentabellen (8 MB) Kernel und Exekutive HAL Treiber Kein Zugriff (64 KB) 3 GB Benutzeradressraum EXE-Code Globale Variablen Threadstacks DLL-Code (2 GB – 64 KB) Abbildung 5–10 Layouts für den virtuellen Adressraum auf x86-Systemen (links 2 GB, rechts 3 GB) Damit ein Prozess über einen Adressraum von 2 GB hinausgehen kann, muss neben der globalen Einstellung increaseuserva auch das Flag IMAGE_FILE_LARGE_ADDRESS_AWARE im Header der Abbilddatei gesetzt sein, da Windows den zusätzliche Adressraum für den Prozess sonst nur reserviert, sodass die Anwendung keine virtuellen Adressen größer als 0x7FFFFFFF sieht. Der Zugriff auf den zusätzlichen virtuellen Speicher muss ausdrücklich verlangt werden, da manche Anwendungen davon ausgehen, dass ihnen höchstens ein Adressraum von 2 GB zur Verfügung gestellt wird. Da das hohe Bit eines Zeigers, der auf Adressen unterhalb von 2 GB verweist, stets 0 ist (für Verweise in einem 2-GB-Adressraum sind nur 31 Bits erforderlich), wird es von diesen Anwendungen als Flag für ihre eigenen Daten verwendet; wobei es natürlich vor dem Verweis auf die Daten zurückgesetzt wird. Wenn solche Anwendungen einen Adressraum von 3 GB verwendeten, würden sie dank dieses Verhaltens Zeiger für Werte oberhalb von 2 GB abschneiden, was zu Programmfehlern einschließlich Datenbeschädigungen führte. Das Flag IMAGE_FILE_LARGE_ADDRESS_AWARE setzen Sie, indem Sie beim Erstellen der ausführbaren Datei das Linkerflag /LARGEADDRESSAWARE angeben. Alternativ können Sie auf der Eigenschaftenseite in Visual Studio auch Linker und dann System auswählen und auf Große Adressen aktivieren klicken. Mit einem Werkzeug wie Editbin.exe, das zu den Windows SDK-Tools gehört, ist es auch möglich, das Flag zu einem ausführbaren Image hinzuzufügen, ohne dieses neu zu erstellen, also ohne dass Sie den Quellcode benötigen, zumindest sofern die Datei nicht signiert ist. Wird die Anwendung auf einem System mit einem Benutzeradressraum von 2 GB ausgeführt, hat dieses Flag keine Auswirkung. 398 Kapitel 5 Speicherverwaltung
Mehrere Systemabbilder verfügen bereits über diese Kennzeichnung zur Verwendung großer Adressräume, sodass sie von der 3-GB-Einstellung profitieren können. Dazu gehören: QQ Lsass.exe Das Teilsystem der lokalen Sicherheitsautorität QQ Inetinfo.exe QQ Chkdsk.exe QQ Smss.exe Internet Information Server Das Hilfsprogramm Check Disk Der Sitzungs-Manager QQ Dllhst3g.exe Eine besondere Version von Dllhost.exe für COM+-Anwendungen Experiment: Die Bereitschaft einer Anwendung zur Verwendung großer Adressräume prüfen Mit dem Programm Dumpbin aus den Visual Studio Tools (und älteren Versionen des Windows SDK) können Sie ausführbare Dateien darauf überprüfen, ob sie große Adressräume nutzen können. Geben Sie zur Anzeige der Ergebnisse den Schalter /headers an. Bei der Anwendung von Dumpbin auf den Sitzungs-Manager ergibt sich die folgende Ausgabe: dumpbin /headers c:\windows\system32\smss.exe Microsoft (R) COFF/PE Dumper Version 14.00.24213.1 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file c:\windows\system32\smss.exe PE signature found File Type: EXECUTABLE IMAGE FILE HEADER VALUES 14C machine (x86) 5 number of sections 57898F8A time date stamp Sat Jul 16 04:36:10 2016 0 file pointer to symbol table 0 number of symbols E0 size of optional header 122 characteristics Executable Application can handle large (>2GB) addresses 32 bit word machine Speicherzuweisungen mit VirtualAlloc, VirtualAllocEx und VirtualAllocExNuma beginnen standardmäßig bei niedrigen virtuellen Adressen und gehen dann immer höher. Ein Prozess erhält höchstens dann hohe virtuelle Adressen, wenn er sehr viel Speicher zuweist oder sein virtueller Adressraum sehr stark fragmentiert ist. Zu Testzwecken können Sie daher erzwingen, dass die Speicherzuweisungen bei hohen Adressen beginnen, indem Sie bei den Virtual­ Alloc*-Funktionen das Flag MEM_TOP_DOWN verwenden oder den DWORD-Registrierungswert ­AllocationPreference zum Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\ Memory Management hinzufügen und auf 0x10000000 setzen. Layouts für virtuelle Adressräume Kapitel 5 399
Die folgende Ausgabe zeigt einen Testlauf des schon in früheren Experimenten verwendeten Programms TestLimit, bei dem es auf einem mit der Option increaseuserva gestarteten 32-Bit-Windows-Computer zu Speicherlecks kommt: Testlimit.exe -r Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - www.sysinternals.com Process ID: 5500 Reserving private bytes (MB)... Leaked 1978 MB of reserved memory (1940 MB total leaked). Lasterror: 8 Der Prozess hat es geschafft, fast – aber nicht ganz – 2 GB zu reservieren. Da der EXE-Code und verschiedene DLLs im Prozessadressraum zugeordnet werden, ist es nicht möglich, dass ein normaler Prozess den gesamten Adressraum reserviert. Auf demselben System können Sie nun zu einem Adressraum von 3 GB wechseln, indem Sie an einer Eingabeaufforderung mit erhöhten Rechten den folgenden Befehl eingeben: C:\WINDOWS\system32>bcdedit /set increaseuserva 3072 The operation completed successfully. Bei diesem Befehl können Sie eine beliebige Größe (in MB) zwischen 2048 MB (dem Standardwert von 2 GB) und 3072 MB (dem Maximum von 3 GB) angeben. Starten Sie das System neu, damit die Änderung in Kraft tritt, und führen Sie TestLimit erneut aus. Jetzt sehen Sie folgendes Ergebnis: Testlimit.exe -r Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - www.sysinternals.com Process ID: 2308 Reserving private bytes (MB)... Leaked 2999 MB of reserved memory (2999 MB total leaked). Lasterror: 8 Wie erwartet, hat TestLimit jetzt fast 3 GB mit Beschlag belegt. Dies ist nur möglich, da das Programm bei der Verlinkung mit dem Flag /LARGEADDRESSAWARE gekennzeichnet worden ist. Anderenfalls hätte sich das Ergebnis nicht von dem auf einem System ohne die Einstellung increaseuserva unterschieden. 400 Kapitel 5 Speicherverwaltung
Um ein System wieder auf die normale Adressraumgröße von 2 GB pro Prozess zurückzusetzen, führen Sie den Befehl bcdedit /deletevalue increaseuserva aus. Das Layout des x86-Systemadressraums Die 32-Bit-Versionen von Windows verwenden ein dynamisches Layout für den Systemadressraum (wobei wir uns den dafür eingesetzten Zuweisungsmechanismus weiter hinten noch genauer ansehen). Wie Sie in Abb. 5–10 sehen, gibt es zwar nach wie vor einige besondere reservierte Bereiche, doch viele Kernelmodusstrukturen nutzen die dynamische Adressraumzuweisung. Daher sind diese Strukturen nicht unbedingt zusammenhängend, sondern können auf verschiedene Bereiche des Systemadressraums verstreut sein. Der auf diese Weise zugewiesene Systemadressraum wird von folgenden Komponenten genutzt: QQ Nicht ausgelagerter Pool QQ Ausgelagerter Pool QQ Besonderer Pool QQ System-PTEs QQ Zugeordnete Systemsichten QQ Dateisystemcache QQ PFN-Datenbank QQ Sitzungsraum x86-Sitzungsraum Da Sitzung 0 von den Systemprozessen und -diensten verwendet wird und Sitzung 1 für den ersten angemeldeten Benutzer, gibt es praktisch auf jedem System mehrere Sitzungen. Der Code und die Daten der einzelnen Sitzungen werden dabei im Systemadressraum zugeordnet, aber immer nur von den Prozessen in der jeweiligen Sitzung gemeinsam genutzt. Abb. 5–11 zeigt den allgemeinen Aufbau des Sitzungsraums. Die Größen der Bestandteile des Sitzungsraums werden ebenso wie im Rest des Kernel-Systemadressraums dynamisch bestimmt und vom Speicher-Manager nach Bedarf angepasst. Sitzungsdatenstrukturen und Arbeitssatz Zuordnungen der Sitzungssicht Ausgelagerter Pool der Sitzung Sitzungstreiberabbilder Win32k.sys Abbildung 5–11 Layout des x86-Sitzungsraums (Darstellung nicht proportional) Layouts für virtuelle Adressräume Kapitel 5 401
Experiment: Sitzungen anzeigen Durch die Untersuchung der Sitzungs-ID können Sie erkennen, welche Prozesse zu welchen Sitzungen gehören. Das können Sie im Task-Manager, in Process Explorer und im Kerneldebugger tun. Im Kerneldebugger können Sie die aktiven Sitzungen wie folgt mit dem Befehl !session auflisten: lkd> !session Sessions on machine: 3 Valid Sessions: 0 1 2 Current Session 2 Anschließend können Sie mit dem Befehl !session -s die aktive Sitzung festlegen und die Adressen der Sitzungsdatenstrukturen und Prozesse mit !sprocess anzeigen lassen: lkd> !session -s 1 Sessions on machine: 3 Implicit process is now d4921040 Using session 1 lkd> !sprocess Dumping Session 1 _MM_SESSION_SPACE d9306000 _MMSESSION d9306c80 PROCESS d4921040 SessionId: 1 Cid: 01d8 Peb: 00668000 ParentCid: 0138 DirBase: 179c5080 ObjectTable: 00000000 HandleCount: 0. Image: smss.exe PROCESS d186c180 SessionId: 1 Cid: 01ec Peb: 00401000 ParentCid: 01d8 DirBase: 179c5040 ObjectTable: d58d48c0 HandleCount: <Data Not Accessible> Image: csrss.exe PROCESS d49acc40 SessionId: 1 Cid: 022c Peb: 03119000 ParentCid: 01d8 DirBase: 179c50c0 ObjectTable: d232e5c0 HandleCount: <Data Not Accessible> Image: winlogon.exe PROCESS dc0918c0 SessionId: 1 Cid: 0374 Peb: 003c4000 ParentCid: 022c DirBase: 179c5160 ObjectTable: dc28f6c0 HandleCount: <Data Not Accessible> Image: LogonUI.exe PROCESS dc08e900 SessionId: 1 Cid: 037c Peb: 00d8b000 ParentCid: 022c DirBase: 179c5180 ObjectTable: dc249640 HandleCount: <Data Not Accessible> Image: dwm.exe → 402 Kapitel 5 Speicherverwaltung
Um Einzelheiten über die Sitzung einzusehen, geben Sie die MM_SESSION_SPACE-Struktur wie folgt mit dem Befehl dt aus: lkd> dt nt!_mm_session_space d9306000 +0x000 ReferenceCount : 0n4 +0x004 u : <unnamed-tag> +0x008 SessionId : 1 +0x00c ProcessReferenceToSession : 0n6 +0x010 ProcessList : _LIST_ENTRY [ 0xd4921128 - 0xdc08e9e8 ] +0x018 SessionPageDirectoryIndex : 0x1617f +0x01c NonPagablePages : 0x28 +0x020 CommittedPages : 0x290 +0x024 PagedPoolStart : 0xc0000000 Void +0x028 PagedPoolEnd : 0xffbfffff Void +0x02c SessionObject : 0xd49222b0 Void +0x030 SessionObjectHandle : 0x800003ac Void +0x034 SessionPoolAllocationFailures : [4] 0 +0x044 ImageTree : _RTL_AVL_TREE +0x048 LocaleId : 0x409 +0x04c AttachCount : 0 +0x050 AttachGate : _KGATE +0x060 WsListEntry : _LIST_ENTRY [ 0xcdcde060 - 0xd6307060 ] +0x080 Lookaside : [24] _GENERAL_LOOKASIDE +0xc80 Session : _MMSESSION ... Experiment: Die Nutzung des Sitzungsraums untersuchen Die Nutzung des Sitzungsraums können Sie sich mit dem Kerneldebuggerbefehl !vm 4 ansehen. Die folgende Ausgabe stammt von einem 32-Bit-Windows-Client-System mit Re­motedesktopverbindung, sodass wir insgesamt drei Sitzungen haben – die beiden Standardsitzungen und die Remotesitzung. (Die Adressen sind die der zuvor vorgestellten MM_SESSION_SPACE-Objekte.) lkd> !vm 4 ... Terminal Server Memory Usage By Session: Session ID 0 @ d6307000: Paged Pool Usage: NonPaged Usage: Commit Usage: 2012 Kb 108 Kb 2292 Kb → Layouts für virtuelle Adressräume Kapitel 5 403
Session ID 1 @ d9306000: Paged Pool Usage: NonPaged Usage: Commit Usage: 2288 Kb 160 Kb 2624 Kb Session ID 2 @ cdcde000: Paged Pool Usage: NonPaged Usage: Commit Usage: 7740 Kb 208 Kb 8144 Kb Session Summary Paged Pool Usage: 12040 Kb NonPaged Usage: Commit Usage: 476 Kb 13060 Kb System-Seitentabelleneinträge System-Seitentabelleneinträge (Page Table Entries, PTEs) werden verwendet, um Systemseiten wie den E/A-Raum, Kernelstacks und Speicherdeskriptorlisten (Memory Descriptor Lists, MDLs; siehe Kapitel 6) dynamisch zuzuordnen. Ihr Vorrat jedoch ist beschränkt. Auf 32-Bit-Windows-Systemen stehen so viele System-PTEs zur Verfügung, um theoretisch einen zusammenhängenden virtuellen Systemadressraum von 2 GB zu beschreiben. Auf der 64-Bit-Version von Windows 10 und auf Windows Server 2016 reichen die System-PTEs für einen zusammenhängenden virtuellen Systemadressraum von 16 TB aus. Experiment: Informationen über System-PTEs einsehen Wie viele System-PTEs zur Verfügung stehen, können Sie sich in der Leistungsüberwachung mit dem Indikator Arbeitsspeicher:Freie Seitentabelleneinträge und mit den Kerneldebuggerbefehlen !sysptes und !vm ansehen. Es ist auch möglich, die _MI_SYSTEM_PTE_TYPE-Struktur als Teil der Speicherstatusvariablen MiState auszugeben (oder der globalen Variablen MiSystemPteInfo auf Windows 8.x/2012/R2). Darin können Sie außerdem erkennen, wie viele PTE-Zuweisungsfehler in dem System aufgetreten sind. Eine hohe Anzahl stellt ein Problem dar und weist auf ein mögliches System-PTE-Leck hin. kd> !sysptes System PTE Information Total System Ptes 216560 starting PTE: c0400000 free blocks: 969 total free: 16334 largest free block: 264 → 404 Kapitel 5 Speicherverwaltung
kd> ? MiState Evaluate expression: -2128443008 = 81228980 kd> dt nt!_MI_SYSTEM_INFORMATION SystemPtes +0x3040 SystemPtes : _MI_SYSTEM_PTE_STATE kd> dt nt!_mi_system_pte_state SystemViewPteInfo 81228980+3040 +0x10c SystemViewPteInfo : _MI_SYSTEM_PTE_TYPE kd> dt nt!_mi_system_pte_type 81228980+3040+10c +0x000 Bitmap : _RTL_BITMAP +0x008 BasePte : 0xc0400000 _MMPTE +0x00c Flags : 0xe +0x010 VaType : c ( MiVaDriverImages ) +0x014 FailureCount : 0x8122bae4 -> 0 +0x018 PteFailures : 0 +0x01c SpinLock : 0 +0x01c GlobalPushLock : (null) +0x020 Vm : 0x8122c008 _MMSUPPORT_INSTANCE +0x024 TotalSystemPtes : 0x120 +0x028 Hint : 0x2576 +0x02c LowestBitEverAllocated : 0xc80 +0x030 CachedPtes : (null) +0x034 TotalFreeSystemPtes : 0x73 Werden viele System-PTE-Fehler angezeigt, können Sie die System-PTE-Verfolgung einschalten, indem Sie im Registrierungsschlüssel HKLM\SYSTEM\CurrentControlSet\Control\ Session Manager\Memory Management den DWORD-Wert TrackPtes erstellen und auf 1 setzen. Mit !sysptes 4 können Sie sich eine Liste der Allozierer ansehen. ARM-Adressraumlayout Wie Sie vin Abb. 5–12 sehen, ist das Adressraumlayout von ARM-Systemen fast mit dem von x86-Systemen identisch. Was die reine Speicherverwaltung angeht, behandelt der Speicher-Manager ARM-Systeme genauso wie x86-Systeme. Die Unterschiede zeigen sich bei der Adressübersetzung, was in dem gleichnamigen Abschnitt weiter hinten in diesem Kapitel erklärt wird. Layouts für virtuelle Adressräume Kapitel 5 405
Für HAL reserviert (4MB) Systemcache Ausgelagerter Pool Nicht ausgelagerter Pool Hyperspace (4 MB) Prozessseitentabellen (4 MB) Kernel und Exekutive HAL Treiber Kein Zugriff (64 KB) EXE-Code Globale Variablen Threadstacks DLL-Code (2 GB – 64 KB) Abbildung 5–12 Layout des virtuellen Adressraums auf ARM-Systemen 64-Bit-Adressraumlayout Theoretisch kann der virtuelle 64-Bit-Adressraum 16 Exabytes oder 18.446.744.073.709.551.616 Bytes umfassen. Heutige Prozessoren erlauben jedoch nur 48 Adresszeilen, was den möglichen Adressraum auf 256 TB (248) beschränkt. Dieser Adressraum ist in der Mitte geteilt, wobei die unteren 128 TB für private Benutzerprozesse und die oberen für das System zur Verfügung stehen. Bei Windows 10 und Windows Server 2016 ist der Systemraum wiederum in mehrere Regio­nen unterschiedlicher Größe aufgeteilt, wie Sie in Abb. 5–13 sehen. Es ist deutlich zu erkennen, dass die 64-Bit-Technik einen enorm viel größeren Adressraum bietet als die 32-Bit-Technik. Die einzelnen Kernelabschnitte müssen allerdings nicht an den gezeigten Stellen beginnen, da in den neuesten Windows-Versionen ASLR im Kernelraum verwendet wird. 406 Kapitel 5 Speicherverwaltung
Für die HAL reserviert 512 GB – 16 TB Nicht ausgelagerter Pool PFN-Datenbank 16 TB System-PTEs Vom Lader initialisierte Zuordnungen Kernel, HAL und alle Nicht-SitzungsTreiber (512 GB) 1 TB Arbeitssatzlisten und Hashtabellen für Systemcache und ausgelagerten Pool 512 GB Besonderer Pool (ausgelagert und nicht ausgelagert) 512 GB – 256 TB Ausgelagerter Pool 16 TB Systemcache Beginn des Systemraums 512 GB – 4 KB WS-Info der System-PTEs Gemeinsame Systemseite (4 KB) 512 GB Seitentabellenzuordnung mit vier Ebenen 16 TB Ultra Zero 1 TB Kernel-CFG-Bitmap Kein Zugriff (nicht kanonische Adressen) 1 TB Hyperspace Kein Zugriff (64 KB) 512 GB Win32k.sys Sitzungsdaten Treiberabbilder Ausgelagerter Pool und Sichten Einzelne Prozesse (128 TB – 64 KB) Abbildung 5–13 Layout des virtuellen Adressraums auf x64-Systemen Windows 8 und Windows Server 2012 sind auf einen Adressraum von 16 TB beschränkt. Das liegt an Einschränkungen bei der Implementierung von Windows, die in Kapitel 10 der 6. Ausgabe von Band 2 erklärt wurden. Von diesen 16 TB entfallen je acht auf den Prozess- und den Systemraum. 32-Bit-Abbilder, die den großen Adressraum verwenden können, genießen bei der Ausführung auf 64-Bit-Windows (unter Wow64) einen besonderen Vorteil, denn daher erhalten sie die kompletten 4 GB des verfügbaren Benutzeradressraums. Wenn das Abbild schon 3-GB-Zeiger unterstützt, sollten 4-GB-Zeiger keinen großen Unterschied mehr ausmachen, da hierbei anders als beim Wechsel von 2 GB zu 3 GB keine zusätzlichen Bits benötigt werden. Die folgende Ausgabe zeigt, wie TestLimit auf einem 64-Bit-Computer als 32-Bit-Anwendung läuft und dabei Adressraum reserviert: C:\Tools\Sysinternals>Testlimit.exe -r Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - www.sysinternals.com Layouts für virtuelle Adressräume Kapitel 5 407
Process ID: 264 Reserving private bytes (MB)... Leaked 4008 MB of reserved memory (4008 MB total leaked). Lasterror: 8 Not enough storage is available to process this command. Das Ergebnis hängt davon ab, ob beim Verlinken von TestLimit die Option /LARGEADDRESSAWARE angegeben wurde. Wäre das nicht der Fall, würde sich für beide Werte 2 GB ergeben. Ohne /LARGEADDRESSAWARE verlinkte 64-Bit-Anwendungen sind ebenso wie 32-Bit-Anwendungen auf die ersten 2 GB des virtuellen Prozessadressraums beschränkt. In Visual Studio wird dieses Flag beim Erstellen von 64-Bit-Anwendungen standardmäßig gesetzt. Einschränkungen bei der virtuellen Adressierung auf x64-Systemen Wie bereits erwähnt, erlaubt die 64-Bit-Technik einen virtuellen Adressraum von bis zu 16 EB, was eine erhebliche Steigerung gegenüber den 4 GB bei der 32-Bit-Adressierung bietet. Weder heute noch in der nahen Zukunft brauchen Computer jedoch auch nur annähernd so viel Speicher. Um die Chiparchitektur zu vereinfachen und einen unnötigen Mehraufwand zu verhindern – insbesondere bei der weiter hinten beschriebenen Adressübersetzung –, verwenden die aktuellen x64-Prozessoren von AMD und Intel lediglich einen virtuellen Adressraum von 256 TB, also nur die unteren 48 Bits der 64-Bit-Adressen. Allerdings sind die Adressen immer noch 64 Bits breit und nehmen 8 Bytes im Register oder im Arbeitsspeicher ein. Ähnlich wie bei der Vorzeichenerweiterung im Zweierkomplement müssen die oberen 16 Bits (Bit 48 bis 64) so gesetzt sein wie das höchste verwendete Bit (Bit 47). Eine Adresse, die diese Regel erfüllt, wird als kanonisch bezeichnet. Die untere Hälfte des Adressraums beginnt bei 0x0000000000000000 und endet bei 0x00007FFFFFFFFFFF, während die obere Hälfte von 0xFFFF800000000000 bis 0xFFFFFFFFFFFFFFFF verläuft. Jeder dieser beiden kanonischen Teile umfasst 128 TB. Wenn moderne Prozessoren mehr Adressbits verwenden, wird die untere Hälfte nach oben bis 0x7FFFFFFFFFFFFFFF ausgedehnt und die obere nach unten bis 0x8000000000000000. Dynamische Verwaltung des virtuellen Systemadressraums Die 32-Bit-Versionen von Windows verwalten den Systemadressraum durch einen internen Kernelallozierer. Die 64-Bit-Versionen benötigen einen solchen Allozierer nicht und können sich deshalb den Aufwand dafür sparen, da alle Regionen im virtuellen Adressraum statisch definiert werden (siehe Abb. 5–13). Bei der Initialisierung des Systems richtet die Funktion MiInitializeDynamicVa die grundlegenden dynamischen Bereiche ein und legt den gesamten verfügbaren Kernelraum als verfügbare virtuelle Adressen fest. Anschließend initialisiert sie die Adressraumbereiche für Bootloaderabbilder, den Prozessraum (Hyperraum) und die HAL. Dazu verwendet sie die Funktion MiInitializeSystemVaRange, die hartcodierte Adressbereiche festlegt (nur auf 32-Bit-Systemen). 408 Kapitel 5 Speicherverwaltung
Bei der später erfolgenden Initialisierung des nicht ausgelagerten Pools wird diese Funktion erneut eingesetzt, um die virtuellen Adressbereiche für den Pool zu reservieren. Wird ein Treiber geladen, so wird die Bezeichnung des Adressbereichs von einem Bootloader- zu einem Treiberabbildbereich geändert. Anschließend kann der Rest des virtuellen Systemadressraums durch MiObtainSystemVa (und ihr Gegenstück MiObtainSessionVa) und MiReturnSystemVa dynamisch angefordert bzw. freigegeben werden. Operationen wie die Erweiterung des Systemcache, der System-PTEs sowie des nicht ausgelagerten, des ausgelagerten und des besonderen Pools, die Zuordnung von großen Seiten, das Erstellen der PFN-Datenbank und das Anlegen einer neuen Sitzung führen alle zu dynamischen Zuweisungen von virtuellen Adressen für einen bestimmten Bereich. Jedes Mal, wenn der Kernelallozierer virtuelle Speicherbereiche für die Verwendung durch eine bestimmte Art von Adressen bezieht, aktualisiert er das Array MiSystemVaType, das den virtuellen Adresstyp für den neu zugewiesenen Bereich angibt. Tabelle 5–8 zeigt die Werte, die in diesem Array auftreten können. Region Beschreibung Begrenzbar MiVaUnused (0) Nicht verwendet – MiVaSessionSpace (1) Adressen für den Sitzungsraum Ja MiVaProcessSpace (2) Adressen für den Prozessadressraum Nein MiVaBootLoaded (3) Adressen für Abbilder, die vom Bootloader geladen wurden Nein MiVaPfnDatabase (4) Adressen für die PFN-Datenbank Nein MiVaNonPagedPool (5) Adressen für den nicht ausgelagerten Pool Ja MiVaPagedPool (6) Adressen für den ausgelagerten Pool Ja MiVaSpecialPoolPaged (7) Adressen für den besonderen Pool (ausgelagert) Nein MiVaSystemCache (8) Adressen für den Systemcache Nein MiVaSystemPtes (9) Adressen für System-PTEs Ja MiVaHal (10) Adressen für die HAL Nein MiVaSessionGlobalSpace (11) Adressen für den globalen Sitzungsraum Nein MiVaDriverImages (12) Adressen für geladene Treiberabbilder Nein MiVaSpecialPoolNonPaged (13) Adressen für den besonderen Pool (nicht ausgelagert) Ja MiVaSystemPtesLarge (14) Adressen für PTEs großer Seiten Ja Tabelle 5–8 Arten von virtuellen Systemadressen Die Möglichkeit, virtuellen Adressraum nach Bedarf dynamisch zu reservieren, ermöglicht zwar eine bessere Verwaltung des virtuellen Speichers, aber wäre nutzlos, wenn nicht auch die Möglichkeit bestünde, diesen Speicher wieder freizugeben. Wenn der ausgelagerte Pool oder der Systemcache verkleinert werden kann oder wenn der besondere Pool und die Zuordnungen großer Seiten freigegeben werden können, werden daher die zugehörigen virtuellen Adressen freigegeben. Ein weiterer solcher Fall liegt vor, wenn die Bootregistrierung freigegeben wird. Layouts für virtuelle Adressräume Kapitel 5 409
Dadurch ist eine dynamische Verwaltung des Speichers nach der Nutzung der einzelnen Komponenten möglich. Außerdem können Komponenten Arbeitsspeicher über MiReclaimSystemVa zurückfordern. Diese Funktion verlangt, dass die mit dem Systemcache verbundenen virtuellen Adressen durch den Thread zur Dereferenzierung auf die Festplatte geleert werden, wenn der verfügbare virtuelle Adressbereich auf unter 128 MB gefallen ist. Die Zurückforderung kann auch erfüllt werden, wenn der ursprünglich nicht ausgelagerte Pool freigegeben wurde. Neben einer besseren Aufteilung und Verwaltung von virtuellen Adressen für die verschiedenen Verbraucher von Kernelspeicherstrukturen bietet die dynamische Zuweisung von virtuellen Adressen auch Vorteile bei der Verringerung des Speicherbedarfs. Anstatt statische Seitentabelleneinträge und Seitentabellen vorab manuell zuzuweisen, werden die entsprechenden Strukturen bei Bedarf zugewiesen. Sowohl auf 32- als auch auf 64-Bit-Systemen verringert das die Speichernutzung beim Hochfahren, da für nicht verwendete Adressen keine Seitentabellen zugewiesen werden müssen. Auf 64-Bit-Systemen müssen auch die Seitentabellen für reservierte große Adressregionen nicht im Speicher zugeordnet werden. Dadurch können sie willkürlich große Grenzwerte haben, was insbesondere auf Systemen von Vorteil ist, auf denen wenig physischer RAM zur Unterstützung der resultierenden Seitenstrukturen zur Verfügung steht. Experiment: Die Nutzung der virtuellen Systemadressen untersuchen (Windows 10 und Windows Server 2016) Die laufende und die Spitzennutzung der einzelnen Arten von virtuellen Systemadressen können Sie sich mit dem Kerneldebugger ansehen. Die globale Variable MiVisibleState (vom Typ MI_VISIBLE_STATE) stellt die in öffentlichen Symbolen verfügbaren Informationen bereit. Das folgende Beispiel stammt von einer x86-Version von Windows 10. 1. Um sich ein Bild der Daten zu machen, die MiVisibleState enthält, geben Sie die Struktur mit Werten aus: lkd> dt nt!_mi_visible_state poi(nt!MiVisibleState) +0x000 SpecialPool : _MI_SPECIAL_POOL +0x048 SessionWsList : _LIST_ENTRY [ 0x91364060 - 0x9a172060 ] +0x050 SessionIdBitmap : 0x8220c3a0 _RTL_BITMAP +0x054 PagedPoolInfo : _MM_PAGED_POOL_INFO +0x070 MaximumNonPagedPoolInPages : 0x80000 +0x074 SizeOfPagedPoolInPages +0x078 SystemPteInfo : 0x7fc00 : _MI_SYSTEM_PTE_TYPE +0x0b0 NonPagedPoolCommit : 0x3272 +0x0b4 BootCommit : 0x186d +0x0b8 MdlPagesAllocated : 0x105 +0x0bc SystemPageTableCommit : 0x1e1 +0x0c0 SpecialPagesInUse : 0 +0x0c4 WsOverheadPages : 0x775 +0x0c8 VadBitmapPages : 0x30 → 410 Kapitel 5 Speicherverwaltung
+0x0cc ProcessCommit : 0xb40 +0x0d0 SharedCommit : 0x712a +0x0d4 DriverCommit : 0n7276 +0x100 SystemWs : [3] _MMSUPPORT_FULL +0x2c0 SystemCacheShared : _MMSUPPORT_SHARED +0x2e4 MapCacheFailures : 0 +0x2e8 PagefileHashPages : 0x30 +0x2ec PteHeader : _SYSPTES_HEADER +0x378 SessionSpecialPool : 0x95201f48 _MI_SPECIAL_POOL +0x37c SystemVaTypeCount : [15] 0 +0x3b8 SystemVaType : [1024] "" +0x7b8 SystemVaTypeCountFailures : [15] 0 +0x7f4 SystemVaTypeCountLimit : [15] 0 +0x830 SystemVaTypeCountPeak : [15] 0 +0x86c SystemAvailableVa 2. : 0x38800000 Am Ende dieses Listings sehen Sie mehrere Arrays mit jeweils 15 Elementen, die die Systemadresstypen aus Tabelle 5–8 angeben. Lassen Sie sich die Arrays SystemVaTypeCount und SystemVaTypeCountPeak anzeigen: lkd> dt nt!_mi_visible_state poi(nt!mivisiblestate) -a SystemVaTypeCount +0x37c SystemVaTypeCount : [00] 0 [01] 0x1c [02] 0xb [03] 0x15 [04] 0xf [05] 0x1b [06] 0x46 [07] 0 [08] 0x125 [09] 0x38 [10] 2 [11] 0xb [12] 0x19 [13] 0 [14] 0xd lkd> dt nt!_mi_visible_state poi(nt!mivisiblestate) -a SystemVaTypeCountPeak +0x830 SystemVaTypeCountPeak : [00] 0 [01] 0x1f [02] 0 → Layouts für virtuelle Adressräume Kapitel 5 411
[03] 0x1f [04] 0xf [05] 0x1d [06] 0x51 [07] 0 [08] 0x1e6 [09] 0x55 [10] 0 [11] 0xb [12] 0x5d [13] 0 [14] 0xe Die Nutzung der virtuellen Systemadressen untersuchen (Windows 8.x und Windows Server 2012/R2) Die laufende und die Spitzennutzung der einzelnen Arten von virtuellen Systemadressen können Sie sich mit dem Kerneldebugger ansehen. Die globalen Kernelarrays MiSystemVaTypeCount, MiSystemVaTypeCountFailures und MiSystemVaTypeCountPeak enthalten die Größe, die Fehlerzähler und die Spitzengrößen für alle Adresstypen aus Tabelle 5–8. Die Größe wird in Mehrfachen einer PDE-Zuweisung angegeben (siehe den Abschnitt »Adressübersetzung« weiter hinten in diesem Kapitel), was unter dem Strich die Größe einer großen Seite ist (2 MB auf x86). Die folgenden Listings zeigen, wie Sie die Nutzung im System und die Spitzennutzung ausgeben können. Eine ähnliche Technik können Sie auch für Fehlerzähler anwenden. Dieses Beispiel stammt von einem 32-Bit-System mit Windows 8.1. lkd> dd /c 1 MiSystemVaTypeCount L f 81c16640 00000000 81c16644 0000001e 81c16648 0000000b 81c1664c 00000018 81c16650 0000000f 81c16654 00000017 81c16658 0000005f 81c1665c 00000000 81c16660 000000c7 81c16664 00000021 81c16668 00000002 81c1666c 00000008 81c16670 0000001c 81c16674 00000000 81c16678 0000000b → 412 Kapitel 5 Speicherverwaltung
lkd> dd /c 1 MiSystemVaTypeCountPeak L f 81c16b60 00000000 81c16b64 00000021 81c16b68 00000000 81c16b6c 00000022 81c16b70 0000000f 81c16b74 0000001e 81c16b78 0000007e 81c16b7c 00000000 81c16b80 000000e3 81c16b84 00000027 81c16b88 00000000 81c16b8c 00000008 81c16b90 00000059 81c16b94 00000000 81c16b98 0000000b Die virtuellen Adressbereiche, die den Komponenten zugewiesen werden, können theoretisch willkürlich wachsen, wenn ausreichend virtueller Systemadressraum zur Verfügung steht. In der Praxis aber bietet der Kernelallozierer auf 32-Bit-Systemen die Möglichkeit, Grenzwerte für die einzelnen Adresstypen festzulegen, um für Zuverlässigkeit und Stabilität zu sorgen. (Auf 64-Bit-Systemen ist eine Erschöpfung des Kerneladressraums zurzeit noch kein Problem.) Standardmäßig sind zwar keine Grenzwerte eingerichtet, doch Systemadministratoren können sie mithilfe der Registrierung für die begrenzbaren Adresstypen (siehe Tabelle 5–8) angeben und ändern. Wenn die aktuelle Anforderung in einem Aufruf von MiObtainSystemVa einen vorhandenen Grenzwert überschreiben würde, wird ein Fehler vermerkt (siehe das vorherige Experiment) und unabhängig vom verfügbaren Speicher eine Rückforderungsoperation angefordert. Das verringert die Speicherlast, sodass die Zuweisung beim nächsten Versuch funktionieren kann. Allerdings wirkt sich die Rückforderung nur auf den Systemcache und den nicht ausgelagerten Pool aus. Experiment: Grenzwerte für virtuelle Systemadressen festlegen Das Array MiSystemVaTypeCountLimit enthält die Grenzwerte für die Nutzung der einzelnen Arten von virtuellen Systemadressen. Zurzeit erlaubt der Speicher-Manager eine Begrenzung nur bei bestimmten Arten von virtuellen Adressen (siehe Tabelle 5–8). Festlegen können Sie diese Grenzwerte mithilfe der Registrierung (siehe http://msdn.microsoft.com/ en-us/library/bb870880.aspx), aber auch dynamisch zur Laufzeit mithilfe eines nicht dokumentierten Systemaufrufs. → Layouts für virtuelle Adressräume Kapitel 5 413
Um die Grenzwerte für die einzelnen Adresstypen einzusehen und festzulegen, können Sie auf 32-Bit-Systemen außerdem das Programm MemLimit aus den Begleitmaterialien zu diesem Buch verwenden. Es zeigt auch die laufende und die Spitzennutzung des virtuellen Adressraums an. Die aktuellen Grenzwerte zeigen Sie mit dem Schalter -q an: C:\Tools>MemLimit.exe -q MemLimit v1.01 - Query and set hard limits on system VA space consumption Copyright (C) 2008-2016 by Alex Ionescu www.alex-ionescu.com System Va Consumption: Type Current Non Paged Pool Peak Limit 45056 KB 55296 KB 0 KB Paged Pool 151552 KB 165888 KB 0 KB System Cache 446464 KB 479232 KB 0 KB System PTEs 90112 KB 135168 KB 0 KB Session Space 63488 KB 73728 KB 0 KB Zum Ausprobieren legen Sie mit dem folgenden Befehl einen Grenzwert von 100 MB für den ausgelagerten Pool fest: memlimit.exe -p 100M Erstellen Sie nun mit dem Sysinternals-Programm TestLimit so viele Handles wie möglich. Wenn ausreichend viel Platz im ausgelagerten Pool zur Verfügung steht, sollte die Höchstzahl bei etwa 16 Millionen liegen. Da der Grenzwert von 100 MB festgelegt ist, erhalten Sie jedoch weniger: C:\Tools\Sysinternals>Testlimit.exe -h Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - www.sysinternals.com Process ID: 4780 Creating handles... Created 10727844 handles. Lasterror: 1450 Weitere Informationen über Objekte, Handles und den Verbrauch des ausgelagerten Pools erhalten Sie in Kapitel 8 von Band 2. 414 Kapitel 5 Speicherverwaltung
Kontingente für den virtuellen Systemadressraum Die im vorigen Abschnitt beschriebenen Grenzwerte für den virtuellen Systemadressraum ermöglichen es, die Nutzung des systemweiten virtuellen Adressraums für bestimmte Kernelkomponenten einzuschränken. Auf 32-Bit-Systemen funktionieren sie jedoch nur, wenn sie systemweit angewendet werden. Um den Wunsch von Systemadministratoren nach einer feineren Kontingentierung zu erfüllen, arbeitet der Speicher-Manager mit dem Prozess-Manager zusammen, sodass für jeden Prozess entweder systemweite oder benutzerspezifische Kontingente durchgesetzt werden können. Um anzugeben, wie viel Speicher jeder Adresstyp eines gegebenen Prozesses nutzen darf, bearbeiten Sie die Werte PagedPoolQuota, NonPagedPoolQuota, PagingFileQuota bzw. WorkingSetPagesQuota im Registrierungsschlüssel HKLM\SYSTEM\CurrentControlSet\Control\ Session Manager\Memory Management. Diese Werte werden bei der Initialisierung gelesen. Daraufhin wird der Standardblock mit dem Systemkontingent erstellt und allen Systemprozessen zugewiesen. Benutzerprozesse erhalten eine Kopie dieses Blocks, sofern nicht wie im Folgenden erklärt Benutzerkontingente festgelegt wurden. Um Benutzerkontingente zu ermöglichen, erstellen Sie unter dem Registrierungsschlüssel HKLM\SYSTEM\CurrentControlSet\Session Manager\Quota System Unterschlüssel für die einzelnen Benutzer-SIDs. In diesen Unterschlüsseln wiederum legen Sie die zuvor erwähnten Werte an, um Grenzwerte nur für die vom jeweiligen Benutzer erstellten Prozesse anzugeben. Tabelle 5–9 zeigt, wie Sie diese Werte einrichten (was auch zur Laufzeit geschehen kann) und welche Rechte dafür erforderlich sind. Wert Beschreibung Einheit Dynamisch Erforderliches Recht PagedPoolQuota Die maximale Größe, die von diesem Prozess aus dem ausgelagerten Pool zugewiesen werden kann Größe in MB Nur für Prozesse, die mit dem Systemtoken ausgeführt werden SeIncreaseQuotaPrivilege NonPagedPoolQuota Die maximale Größe, die von diesem Prozess aus dem nicht ausgelagerten Pool zugewiesen werden kann Größe in MB Nur für Prozesse, die mit dem Systemtoken ausgeführt werden SeIncreaseQuotaPrivilege PagingFileQuota Die maximale Anzahl der Seiten, die der Prozess in der Auslagerungsdatei haben kann Seiten Nur für Prozesse, die mit dem Systemtoken ausgeführt werden SeIncreaseQuotaPrivilege Layouts für virtuelle Adressräume → Kapitel 5 415
Wert Beschreibung Einheit Dynamisch Erforderliches Recht WorkingSetPagesQuota Die maximale Anzahl der Seiten, die der Prozess in seinem Arbeitssatz (im physischen Speicher) ­haben kann Seiten Ja SeIncreaseBasePriorityPrivilege, es sei denn, bei der Operation handelt es sich um eine Bereinigungsanforderung Tabelle 5–9 Registrierungswerte für Prozesskontingente Layout des Benutzeradressraums Ebenso wie der Kernel- ist auch der Benutzeradressraum dynamisch aufgebaut. Die Adressen der Threadstacks, Prozessheaps und geladenen Abbilder (wie DLLs und ausführbare Dateien der Anwendung) werden durch den ASLR-Mechanismus dynamisch berechnet (sofern die Anwendung und ihre Abbilder das zulassen). Auf der Ebene des Betriebssystems ist der Benutzeradressraum in einige wenige genau abgegrenzte Regionen aufgeteilt (siehe Abb. 5–14). Die ausführbaren Dateien und DLLs sind in Form von im Speicher zugeordneten Abbilddateien vorhanden, gefolgt von den Prozessheaps und Threadstacks. Abgesehen von diesen Regionen (und einigen reservierten Systemstrukturen wie den TEBs und dem PEB) sind alle anderen Speicherzuweisungen laufzeitabhängig und erfolgen zur Laufzeit. An der Platzierung all dieser laufzeitabhängigen Regionen ist ASLR beteiligt. Zusammen mit der Datenausführungsverhinderung erschwert dieser Mechanismus einen Remoteangriff auf das System durch Manipulation des Arbeitsspeichers. Da sich der Code und die Daten von Windows an dynamisch festgelegten Orten befinden, kann ein Angreifer keinen sinnvollen hartcodierten Offset in ein Programm oder eine DLL einschleusen. Threadstack Zufällig ausgewählte Ladeadresse Zufällig ausgewählte Adresse DLLs mit dynamischer Basisadresse Standardprozessheap Threadstack Threadstack Zufällig ausgewählte Ladeadresse Ausführbare Datei Abbildung 5–14 Layout des Benutzeradressraums mit ASLR 416 Kapitel 5 Speicherverwaltung
Experiment: Den virtuellen Benutzeradressraum analysieren Das Sysinternals-Programm VMMap zeigt eine ausführliche Ansicht des virtuellen Arbeitsspeichers, den ein beliebiger Prozess auf Ihrem Computer verwendet. Diese Informationen sind nach den einzelnen Arten von Zuweisungen in folgende Kategorien gegliedert: QQ Image Zeigt die Speicherzuweisungen für die Zuordnung der ausführbaren Datei und ihrer Abhängigkeiten (wie dynamischen Bibliotheken) sowie anderer im Speicher zugeordneter Abbilddateien (PE-Format) an. QQ Mapped File Zeigt Zuweisungen für im Speicher zugeordnete Datendateien an. QQ Shareable Zeigt als gemeinsam nutzbar gekennzeichnete Speicherzuweisungen an. Dazu gehört gewöhnlich der gemeinsam genutzte Speicher (allerdings nicht die im Speicher zugeordneten Dateien, die unter Image oder Mapped File aufgeführt werden). QQ Heap Zeigt Speicherzuweisungen für die Heaps an, die der Prozess besitzt. QQ Managed Heap Zeigt Speicherzuweisungen durch die .NET-CLR an (verwaltete Objekte). Bei einem Prozess, der .NET nicht nutzt, wird hier nichts angezeigt. QQ Stack Zeigt Speicherzuweisungen für die Stacks der einzelnen Threads in diesem Prozess an. QQ Private Data Zeigt als privat gekennzeichnete Speicherzuweisungen außer den Stacks und Heaps an, z. B. interne Datenstrukturen. Der folgende Screenshot zeigt eine typische Ansicht des Explorers (64 Bit) in VMMap: → Layouts für virtuelle Adressräume Kapitel 5 417
Je nach Art der Speicherzuweisung kann VMMap auch noch weitere Informationen wie Dateiname (für zugeordnete Dateien), Heap-ID und -Typ (für Heapzuweisungen) und Thread-ID (für Stackzuweisungen) anzeigen. Außerdem werden sowohl beim zugesicherten Speicher als auch beim Arbeitssatzspeicher die Kosten der Zuweisung angegeben. Auch Größe und Schutz der einzelnen Zuweisungen werden gezeigt. ASLR beginnt auf Abbildebene mit der ausführbaren Datei für den Prozess und ihren abhängigen DLLs. Sie verarbeitet sämtliche Abbilddateien, die über einen Relokalisierungsabschnitt verfügen und in deren PE-Header die Unterstützung für ASLR angegeben ist (durch das Flag IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE, das gewöhnlich mit dem Linkerflag /DYNAMICBASE in Microsoft Visual Studio gesetzt wird). Für ein solches Abbild wählt das System aus einem Bereich von 256 Werten, die alle an 64-Bit-Grenzen ausgerichtet sind, einen für den aktuellen Boot global gültigen Abbildoffset aus. Abbildrandomisierung Um den Ladeoffset für ausführbare Dateien zu bestimmen, wird jedes Mal, wenn eine solche Datei geladen wird, ein Deltawert berechnet. Dabei handelt es sich um eine 8-Bit-Pseudozufallszahl von 0x10000 bis 0xFE0000. Dazu wird der aktuelle Zeitstempelzähler (Time Stamp Counter, TSC) des Prozessors um vier Stellen verschoben, auf das Ergebnis eine Division modulo 254 angewendet und 1 addiert. Diese Zahl wiederum wird mit der bekannten Zuweisungsgranularität von 64 KB multipliziert. Durch die Addition von 1 ist garantiert, dass der Wert nie 0 sein kann, weshalb ausführbare Dateien bei der Verwendung von ASLR niemals an der im PE-Header angegebenen Adresse geladen werden. Das Delta wird dann zu der bevorzugten Ladeadresse der ausführbaren Datei addiert. Damit sind 256 Stellen in einem Bereich von 16 MB von der Abbildadresse im PE-Header herum möglich. Die Berechnung des Ladeoffsets für DLLs beginnt mit einem systemweiten Wert für den jeweiligen Boot, der als Abbildverschiebung bezeichnet wird. Er wird von MiInitializeRelocations berechnet und in den MiState.Sections.ImageBias-Feldern in der globalen Speicherstatusstruktur (MI_SYSTEM_INFORMATION) festgehalten (bzw. in der globalen Variablen MiImageBias in Windows 8.x/2012/R2). Bei diesem Wert handelt es sich um den TSC der aktuellen CPU beim Aufruf dieser Funktion im Bootzyklus, der verschoben und zu einem 8-Bit-Wert maskiert wurde. Dadurch sind auf 32-Bit-Systemen 256 verschiedene Werte möglich. Ähnliche Berechnungen erfolgen auch auf 64-Bit-Systemen, wo es aufgrund des riesigen Adressraums jedoch mehr mögliche Werte gibt. Anders als bei ausführbaren Dateien wird dieser Wert nur einmal pro Boot berechnet und dann im gesamten System verwendet, damit DLLs weiterhin im physischen Speicher gemeinsam genutzt werden können und nur einmal relokalisiert werden. Würden DLLs in den einzelnen Prozessen an verschiedenen Stellen neu zugeordnet, könnte ihr Code nicht gemeinsam genutzt werden. Der Lader müsste die Adressverweise für jeden Prozess einzeln anpassen. Aus gemeinsam nutzbarem, schreibgeschütztem Code würden dadurch private Prozessdaten. Jeder Prozess, der eine DLL nutzt, müsste seine eigene private Kopie von ihr im physischen Speicher haben. 418 Kapitel 5 Speicherverwaltung
Nach der Berechnung des Offsets initialisiert der Speicher-Manager die Bitmap ImageBitMap (bzw. die globale Variable MiImageBitMap in Windows 8.x/2012/R2), die zur MI_SECTION_ STATE-Struktur gehört. Diese Bitmap stellt die Bereiche von 0x50000000 bis 0x78000000 auf 32-Bit-Systemen dar (die Zahlen für 64-Bit-Systeme finden Sie weiter unten). Jedes Bit steht dabei für eine Zuweisungseinheit (64 KB). Wenn der Speicher-Manager eine DLL lädt, wird das entsprechende Bit gesetzt, um die Platzierung im System zu kennzeichnen. Wird dieselbe DLL erneut geladen, verwendet der Speicher-Manager für sie das gleiche Abschnittsobjekt wie für die bereits relokalisierten Daten. Beim Laden einer DLL durchsucht das System die Bitmap von oben nach unten nach freien Bits. Der zuvor berechnete ImageBias-Wert wird als Startindex von oben verwenden, damit wie bereits angedeutet bei jedem Boot eine andere, zufällige Platzierung erreicht wird. Da die Bitmap beim Laden der ersten DLL (bei der es sich stets um Ntdll.dll handelt) leer ist, kann deren Ladeadresse leicht berechnet werden (wobei 64-Bit-Systeme einen anderen Verschiebungswert haben): QQ 32 Bit 0x78000000 - (ImageBias + NtDllSizein64KBChunks) * 0x10000 QQ 64 Bit 0x7FFFFFFF0000 – (ImageBias64High + NtDllSizein64KBChunks) * 0x10000 Jede nachfolgende DLL wird dann jeweils in einem tiefer liegenden 64-KB-Teilstück geladen. Aus diesem Grund lassen sich die Adressen der anderen DLLs leicht berechnen, wenn die Adresse von Ntdll.dll bekannt ist. Um diese Gefahr zu verringern, wird auch die Reihenfolge, in der die Standard-DLLs vom Sitzungs-Manager während der Initialisierung zugeordnet werden, beim Laden von Smss.exe randomisiert. Ist in der Bitmap kein freier Platz mehr vorhanden (das heißt, wird bereits der Großteil der für ASLR definierten Region verwendet), schaltet der DLL-Relokalisierungscode auf die Vorgehensweise für ausführbare Dateien um und lädt die DLLs in 64-KB-Teilstücken im Umkreis von 16 MB um die bevorzugte Basisadresse. Experiment: Die Ladeadresse von Ntdll.dll berechnen Mit den Informationen über die Ladeadresse aus der im letzten Abschnitt genannten Kernelvariablen können Sie die Ladeadresse von Ntdll.dll berechnen. Die folgende Anleitung gilt für ein x86-System mit Windows 10: 1. Starten Sie den lokalen Kerneldebugger. 2. Ermitteln Sie den Wert von ImageBias: lkd> ? nt!mistate Evaluate expression: -2113373760 = 820879c0 lkd> dt nt!_mi_system_information sections.imagebias 820879c0 +0x500 Sections : +0x0dc ImageBias : 0x6e → Layouts für virtuelle Adressräume Kapitel 5 419
3. Öffnen Sie den Explorer und ermitteln Sie die Größe von Ntdll.dll im Verzeichnis System32. Auf dem Beispielsystem beträgt sie 1547 KB = 0x182c00, was einem Wert von 0x19 in 64-KB-Stücken entspricht (aufgerundet). Es ergibt sich also 0x78000000 – (0x6E + 0x19) * 0x10000 = 0x77790000. 4. Öffnen Sie Process Explorer und schauen Sie sich die Ladeadresse eines beliebigen Prozesses von Ntdll.dll an (in der Spalte Base oder Image Base). Es sollte der eben ermittelte Wert angezeigt werden. 5. Probieren Sie das Gleiche auf einem 64-Bit-System aus. Stackrandomisierung Sofern nicht das Flag StackRandomizationDisabled für den Prozess gesetzt ist, wird im nächsten Schritt der ASLR der Speicherort des Stacks für den ursprünglichen Thread und später aller neuen Threads randomisiert. Dabei wird zunächst einer der 32 möglichen Stackorte ausgewählt, die 64 KB oder 256 KB weit auseinanderliegen. Zur Auswahl dieser Grundadresse wird die erste geeignete freie Speicherregion gesucht und dann die x-te verfügbare Region ausgewählt, wobei x wiederum auf dem aktuellen TSC des Prozessors beruht, der verschoben und zu einem 5-Bit-Wert maskiert wird, was 32 mögliche Orte ergibt. Nach der Auswahl dieser Grundadresse wird ein neuer Wert auf der Grundlage des TSCs berechnet, diesmal mit einer Länge von 9 Bits. Dieser Wert wird mit 4 multipliziert, um die Ausrichtung zu erhalten. Dadurch kann der Wert maximal 2048 Bytes (eine halbe Seite) groß werden. Um die endgültige Basisadresse des Stacks zu bestimmen, wird dieser Wert zu der zuvor ermittelten Grundadresse addiert. Heaprandomisierung ASLR randomisiert auch den Speicherort des ursprünglichen und aller nachfolgenden Heaps eines Prozesses, die im Benutzermodus erstellt wurden. Die Funktion RtlCreateHeap verwendet einen weiteren Pseudozufallswert auf der Grundlage des TSCs, um die Grundadresse zu bestimmen. Dieser 5 Bits lange Wert wird mit 64 KB multipliziert, um die endgültige Basisadresse des Heaps festzulegen. Der Bereich für diesen Wert beginnt bei 0, weshalb sich für den ursprünglichen Heap ein Adressbereich von 0x00000000 bis 0x001F0000 ergibt. Außerdem wird die Zuweisung des Bereichs vor der Basisadresse des Heaps manuell aufgehoben, um bei einem Brute-Force-Angriff über den gesamten möglichen Heapadressbereich eine Zugriffsverletzung zu erzwingen. ASLR im Kerneladressraum ASLR ist auch im Kerneladressraum aktiv. Dort gibt es 64 mögliche Ladeadressen für 32-Bit-Treiber und 256 für 64-Bit-Treiber. Die Relokalisierung von Benutzerraumabbildern erfordert eine erhebliche Menge an Rangierplatz im Kernelraum, aber wenn der Kernelraum knapp ist, kann ASLR auch Benutzermodus-Adressraum des Systemprozesses als Rangierplatz nutzen. Auf Windows 10 (Version 1607) und Windows Server 2016 ist ASLR für die meisten Systemspeicher- 420 Kapitel 5 Speicherverwaltung
regionen eingerichtet, z. B. den ausgelagerten und nicht ausgelagerten Pool, den Systemcache, die Seitentabellen und die PFN-Datenbank (initialisiert von MiAssignTopLevelRanges). Sicherheitsmaßnahmen verwalten Wegen ihrer möglichen Auswirkungen auf die Kompatibilität sind ASLR und viele andere Sicherheitsmaßnahmen in Windows optional: ASLR wird nur auf Abbilder angewendet, in deren Header das Bit IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE gesetzt ist, der Hardware-Ausführungsschutz wird durch Boot- und Linkeroptionen gesteuert usw. Um sowohl Unternehmenskunden als auch Endverbrauchern einen Überblick und eine Steuerung dieser Funktionen zu ermöglichen, hat Microsoft das Enhanced Mitigation Experience Toolkit (EMET) herausgegeben. Es ermöglicht eine zentrale Verwaltung der in Windows eingebauten Sicherheitsmaßnahmen und bietet darüber hinaus mehrere zusätzliche Maßnahmen, die in Windows noch nicht enthalten sind. Des Weiteren kann EMET Administratoren über das Ereignisprotokoll informieren, wenn bei einer Software aufgrund der angewendeten Sicherheitsmaßnahmen Zugriffsfehler aufgetreten sind. In EMET können Sie auch einzelne Anwendungen, die in bestimmten Umgebungen Kompatibilitätsprobleme haben, von den Sicherheitsmaßnahmen ausnehmen, die die Entwickler für sie aktiviert haben. Zurzeit ist EMET bei Version 5.5 angelangt. Das Ende des Supports wurde für Juli 2018 angekündigt, allerdings wurden einige Features in aktuelle Windows-Versionen aufgenommen. Experiment: Den ASLR-Schutz für Prozesse untersuchen Mit dem Sysinternals-Werkzeug Process Explorer können Sie sich Ihre Prozesse (und vor allem die von ihnen geladenen DLLs) ansehen, um festzustellen, ob sie ASLR unterstützen. Auch wenn nur eine DLL keine Randomisierung zulässt, kann das den Prozess anfälliger für Angriffe machen. Um sich den ASLR-Status der Prozesse anzusehen, gehen Sie wie folgt vor: 1. Rechtsklicken Sie im Prozessbaum auf eine beliebige Spalte und wählen Sie Select ­Columns. 2. Wählen Sie auf den Registerkarten Process Image und DLL jeweils ASLR Enabled aus. Wie Sie sehen, laufen alle im Lieferumfang von Windows enthaltenen Programme und Dienste mit aktiviertem ASLR. Manche Drittanbieteranwendungen werden ebenfalls mit ASLR ausgeführt, andere dagegen nicht. In dem folgenden Beispiel ist der Prozess Notepad.exe hervorgehoben. Seine Lade­ adresse lautet hier 0x7FF7D76B0000. Wenn Sie alle Instanzen von Notepad schließen und eine neue öffnen, werden Sie eine andere Ladeadresse sehen. Wenn Sie das System herunterfahren und neu starten und das Experiment dann erneut ausführen, befinden sich die ASLR-fähigen DLLs nach jedem Startvorgang an anderen Ladeadressen. → Layouts für virtuelle Adressräume Kapitel 5 421
Adressübersetzung Sie wissen jetzt, wie Windows den virtuellen Adressraum strukturiert. Als Nächstes sehen wir uns an, wie es diese Adressräume physischen Seiten zuordnet. Benutzeranwendungen und Systemcode verweisen auf virtuelle Adressen. Dieser Abschnitt beginnt mit einer ausführlichen Beschreibung der 32-Bit-x86-Adressübersetzung im PAE-Modus (der als einziger in aktuellen Windows-Versionen unterstützt wird), gefolgt von einer Erläuterung der Unterschiede zu den Plattformen ARM und x64. Im nächsten Abschnitt – »Seitenfehler« – wird dann erklärt, was geschieht, wenn bei einer solchen Übersetzung die Auflösung zu einer physischen Speicheradresse nicht möglich ist, und wie Windows den physischen Arbeitsspeicher mithilfe von Arbeitssätzen und der Seitenframedatenbank verwaltet. Übersetzung virtueller Adressen auf x86-Systemen Angelehnt an die zur damaligen Zeit verfügbare CPU-Hardware hat der ursprüngliche x86-Kernel nicht mehr als 4 GB physischen Arbeitsspeicher unterstützt. Mit dem x86-Prozessor Intel Pentium Pro wurde der neue Speicherzuordnungsmodus der physischen Adresserweiterung (Physical Address Extension, PAE) eingeführt. Auf den heutigen x86-Prozessoren von Intel und einem geeigneten Chipsatz ermöglicht dieser Modus 32-Bit-Betriebssystemen, auf bis zu 64 GB physischen Arbeitsspeicher zuzugreifen (statt auf lediglich 4 GB ohne PAE). Auf x64-Prozessoren im Legacy-Modus wären damit sogar 1024 GB zugänglich, was von Windows zurzeit aber auf 64 GB eingeschränkt wird, da zur Beschreibung einer solchen Menge an Speicher eine übermäßig große PFN-Datenbank erforderlich wäre. Lange Zeit hat Windows zwei verschiedene x86-Kernels gehabt, nämlich einen mit PAE-Unterstützung und einen ohne. Seit Windows 422 Kapitel 5 Speicherverwaltung
Vista wird bei einer x86-Version von Windows immer der PAE-Kernel installiert, auch wenn der physische Arbeitsspeicher des Systems nicht mehr als 4 GB umfasst. Die Leistungs- und Speicherbedarfsvorteile des Nicht-PAE-Kernels sind ohnehin vernachlässigbar geworden, und für den Hardware-Ausführungsschutz ist überdies der PAE-Kernel erforderlich. Dank dieser Entscheidung muss Microsoft nur einen einzigen x86-Kernel pflegen. Aus diesem Grund beschreiben wir hier nur die x86-Adressübersetzung im PAE-Modus. Wenn Sie sich für den Nicht-PAEFall interessieren, können Sie die entsprechenden Abschnitte in der 6. Ausgabe dieses Buches nachlesen. Mithilfe der vom Speicher-Manager erstellten und gepflegten Seitentabellen übersetzt die CPU virtuelle in physische Adressen. Eine Seite des virtuellen Adressraums ist mit einem Seitentabelleneintrag (Page Table Entry, PTE) im Systemraum verknüpft, der die physische Adresse angibt, auf die die virtuelle abgebildet wird. In dem Beispiel aus Abb. 5–15 sehen Sie drei aufeinanderfolgende Seiten, die auf einem x86-System drei unzusammenhängenden physischen Seiten zugeordnet sind. Es kann sein, dass es für Regionen, die als reserviert oder zugesichert gekennzeichnet sind, auf die aber noch nie zugegriffen wurde, gar keine PTEs gibt, da die Seitentabelle selbst unter Umständen erst beim Auftreten des ersten Seitenfehlers zugewiesen wird. Die gestrichelte Linie, die in Abb. 5–15 die virtuellen Seiten mit den PTEs verbindet, stellt die indirekte Beziehung zwischen den virtuellen Seiten und dem physischen Speicher dar. Seiten­ tabellen­ einträge Prozessseitentabellen Physischer Speicher (RAM) Virtuelle Seiten Abbildung 5–15 Zuordnung virtueller Adressen im physischen Speicher (x86) Adressübersetzung Kapitel 5 423
Selbst Kernelmoduscode (z. B. Gerätetreiber) ist nicht in der Lage, direkt auf physische Speicheradressen zu verweisen, sondern muss dies indirekt tun, indem er erst die virtuellen Adressen erstellt, die den physischen zugeordnet sind. Weitere Informationen finden Sie in der Beschreibung der Unterstützungsroutinen für die Speicherdeskriptorliste (Memory Descriptor List, MDL) in der WDK-Dokumentation. Der eigentliche Übersetzungsvorgang und der Aufbau der Seitentabellen und -verzeichnisse werden von der CPU bestimmt. Das Betriebssystem muss diese Strukturen anschließend wie vorgesehen im Speicher erstellen. Abb. 5–16 zeigt ein allgemeines Diagramm der x86-Adressübersetzung. Das allgemeine Prinzip ist jedoch auf allen Architekturen das gleiche. Wie Sie in Abb. 5–16 sehen, besteht die Eingabe in das Übersetzungssystem aus einer virtuellen 32-Bit-Adresse und einer Reihe von Speicherstrukturen wie Seitentabellen, Seitenverzeichnissen, einer einzelnen Seitenverzeichnis-Zeigertabelle (Page Directory Pointer Table, PDPT) und Look-Aside-Übersetzungspuffern (Translation Look Aside Buffer, TLB). Die Ausgabe ist die physische 36-Bit-Adresse im RAM, an der sich das eigentliche Byte befindet. Die Zahl 36 kommt durch die Art und Weise zustande, wie die Seitentabellen strukturiert sind, was vom Prozessor bestimmt wird. Bei der Zuordnung kleiner Seiten (was der übliche und in Abb. 5–16 gezeigte Fall ist) werden die 12 am wenigsten signifikanten Bits der virtuellen Adresse direkt in die resultierende physische Adresse kopiert. 12 Bits entsprechen genau 4 KB, also der Größe einer kleinen Seite. Seiten­ verzeichnis­zeiger Seiten­ verzeichnisse Virtuelle Adresse (32 Bit) Nummer der virtuellen Seite Adressübersetzung (CPU) Wenn die Seite nicht gültig ist ... Seiten­tabellen Look-AsideÜbersetzungs­ puffer (TLB) Byte auf der Seite Nummer der physischen Seite Seitenfehler Byte auf der Seite Physische Adresse (36 Bit) Abbildung 5–16 Überblick über die Übersetzung virtueller Adressen Wenn eine Adresse nicht übersetzt werden kann (etwa wenn sich die Seite nicht im physischen Speicher befindet, sondern in der Auslagerungsdatei), löst die CPU eine Ausnahme aus – einen sogenannten Seitenfehler –, um dem Betriebssystem mitzuteilen, dass es die Seite nicht finden kann. Da die CPU keine Ahnung hat, wo sich die Seite befindet (Auslagerungsdatei, zugeordnete Datei oder anderswo), wartet sie darauf, dass das Betriebssystem die Seite von dem Ort abruft, an dem sie liegt (falls das möglich ist), und die Seitentabellen korrigiert, sodass sie 424 Kapitel 5 Speicherverwaltung
auf die Seite zeigen. Anschließend versucht die CPU erneut, die Übersetzung durchzuführen. Seitenfehler werden weiter hinten in diesem Kapitel im gleichnamigen Abschnitt ausführlicher erklärt. Abb. 5–17 zeigt den kompletten Vorgang der Übersetzung von virtuellen in physische x86-Adressen. Virtuelle 32-Bit-Adresse 512 Einträge SeitenverzeichnisZeigertabelle 512 Einträge Seite Byte auf der Seite Seitenverzeichnisse Seitentabellen Abbildung 5–17 Übersetzung virtueller Adressen auf einem x86-System Der zu übersetzende virtuelle 32-Bit-Adressraum ist logisch in vier Teile gegliedert. Wie Sie bereits gelernt haben, werden die unteren 12 Bits so wie sie sind verwendet, um ein bestimmtes Byte auf einer Seite auszuwählen. Der Übersetzungsvorgang beginnt mit einer einzigen PDPT pro Prozess, die sich stets im physischen Speicher befindet (denn sonst könnte das System sie nicht finden). Ihre physische Adresse ist jeweils in der KPROCESS-Struktur ihres Prozesses gespeichert. Das besondere x86-Register CR3 hält diesen Wert für den zurzeit ausgeführten Prozess fest (d. h. für den Prozess, zu dem der Thread gehört, der auf die virtuelle Adresse zugegriffen hat). Wenn auf der CPU ein Kontextwechsel erfolgt und der neue Thread in einem anderen Prozess läuft als der vorherige, muss das Register CR3 mit der PDPT-Adresse des neuen Prozesses aus dessen KPROCESS-Struktur geladen werden. Die PDPT muss an einer 32-Byte-Grenze ausgerichtet sein und sich in den ersten 4 GB des RAM befinden (da CR3 auf x86 nach wie vor ein 32-Bit-Register ist). Bei einem Adressaufbau wie in Abb. 5–17 erfolgt die Übersetzung wie folgt: 1. Die beiden signifikantesten Bits der virtuellen Adresse (Bit 30 und 31) bilden einen Index für die PDPT. Diese Tabelle hat vier Einträge. Der ausgewählte Eintrag – also der Seitenverzeichnis-Zeigereintrag oder PDPE – zeigt auf die physische Adresse eines Seitenverzeichnisses. 2. Ein Seitenverzeichnis enthält 512 Einträge, von denen einer durch die neun Bits von 21 bis 29 der virtuellen Adresse ausgewählt wird. Dieser Seitenverzeichniseintrag (Page Directory Entry, PDE) zeigt auf die physische Adresse einer Seitentabelle. Adressübersetzung Kapitel 5 425
3. Eine Seitentabelle enthält ebenfalls 512 Einträge, von denen einer durch die neun Bits von 12 bis 20 der virtuellen Adresse ausgewählt wird. Dieser Seitentabelleneintrag (PTE) zeigt auf die physische Adresse des Seitenanfangs. 4. Zu der Adresse, auf die der PTE zeigt, wird der Offset (die unteren 12 Bits) addiert, um die endgültige, vom Aufrufer angeforderte physische Adresse zu erhalten. Die Einträge in den verschiedenen Tabellen werden auch als Seitenframenummern (Page Frame Numbers, PFNs) bezeichnet, da sie auf an Seitengrenzen ausgerichtete Adressen zeigen. Jeder Eintrag ist 64 Bits breit, weshalb das Seitenverzeichnis und die Seitentabelle selbst nicht größer als eine 4-KB-Seite sind. Um den physischen Bereich von 64 GB zu beschreiben, sind jedoch nur 24 Bits erforderlich (wenn sie mit den 12 Bits für den Offset für einen Adressbereich von insgesamt 36 Bits kombiniert werden). Es sind also mehr als genug Bits für die eigentlichen PFN-Werte vorhanden. Eines der zusätzlichen Bits ist für den gesamten Mechanismus von herausragender Bedeutung, nämlich das Gültigkeitsbit. Es gibt an, ob die PFN-Daten überhaupt gültig sind und ob die CPU den beschriebenen Vorgang ausführen soll. Ist das Bit nicht gesetzt, bedeutet das einen Seitenfehler. Die CPU löst eine Ausnahme aus und erwartet, dass sich das Betriebssystem auf sinnvolle Weise um den Seitenfehler kümmert. Wenn beispielsweise die fragliche Seite auf die Festplatte geschrieben wurde, liest der Speicher-Manager sie wieder in eine freie Seite im RAM zurück, korrigiert den PTE und weist die CPU an, es erneut zu versuchen. Da Windows für jeden Prozess einen privaten Adressraum bereitstellt, hat jeder Prozess auch seine eigene PDPT, seine eigenen Seitenverzeichnisse und Seitentabellen, um diesen privaten Adressraum zuzuordnen. Die Seitenverzeichnisse und Seitentabellen, die den Systemraum beschreiben, werden jedoch von allen Prozessen gemeinsam genutzt (und die für den Sitzungsraum von allen Prozessen der zugehörigen Sitzung). Damit nicht mehrere Seitentabellen denselben virtuellen Speicher beschreiben, werden die Seitenverzeichniseinträge für den Systemraum beim Erstellen eines Prozesses so initialisiert, dass sie auf die vorhandenen Systemseitentabellen zeigen. Gehört der Prozess zu einer Sitzung, zeigen seine Seitenverzeichniseinträge auf die bereits vorhandenen Seitentabellen für die Sitzung, damit diese gemeinsam genutzt werden können. Seitentabellen und Seitentabelleneinträge Jeder PDE zeigt auf eine Seitentabelle. Jede Seitentabelle – und auch die PDPT – ist ein einfaches Array aus PTEs. Sie verfügt über 512 Einträge, die jeweils eine einzelne Seite (4 KB) zuordnen. Daher kann eine Seitentabelle einen Adressraum von 2 MB (512 x 4 KB) zuordnen. Auch ein Seitenverzeichnis besteht aus 512 Einträgen, die jeweils auf eine Seitentabelle zeigen. Das bedeutet, dass ein Seitenverzeichnis einen Adressraum von 512 x 2 MB = 1 GB zuordnen kann. Mit den vier PDPEs lässt sich daher der gesamte 32-Bit-Adressraum von 4 GB zuordnen. Bei großen Seiten zeigt der PDE mit 11 Bits auf den Seitenanfang im physischen Speicher. Der Byteoffset wird aus den unteren 21 Bits der ursprünglichen virtuellen Adresse entnommen. Ein PDE für die Zuordnung einer großen Seite zeigt also nicht auf eine Seitentabelle. Der Aufbau der Seitenverzeichnisse und -tabellen ist im Grunde genommen identisch. Mit dem Kerneldebuggerbefehl !pte können Sie sich PTEs genauer ansehen (siehe das nachfolgen- 426 Kapitel 5 Speicherverwaltung
de Experiment »Adressen übersetzen«). Hier wollen wir uns mit gültigen PTEs beschäftigen und die ungültigen auf den Abschnitt »Seitenfehler« verschieben. Gültige PTEs bestehen aus zwei Hauptteilen, nämlich der PFN der physischen Seite mit den Daten oder der physischen Adresse einer Seite im Arbeitsspeicher sowie einigen Flags, die den Status und den Schutz der Seite beschreiben (siehe Abb. 5–18). Modifiziert Große Seite (bei PDE) Zugriff erfolgt Global Cache deaktiviert Kopieren beim Schreiben (Software) Write-Through Prototyp (Software) Besitzer Schreiben (Software) Schreiben Keine Ausführung Gültig Reserviert Abbildung 5–18 Aufbau eines gültigen x86-Hardware-PTEs Die in Abb. 5–18 mit »Software« und »Reserviert« gekennzeichneten Bits werden von der Speicherverwaltungseinheit (Memory Management Unit, MMU) in der CPU unabhängig davon ignoriert, ob der PTE gültig ist. Diese Bits werden vom Speicher-Manager gespeichert und interpretiert. Tabelle 5–10 gibt die hardwaredefinierten Bits in einem gültigen PTE an. Bit Bedeutung Zugriff erfolgt (Accessed) Es wurde auf die Seite zugegriffen. Cache deaktiviert (Cache disabled) Deaktiviert die CPU-Zwischenspeicherung für die Seite. Kopieren beim Schreiben (Copy-on-Write) Die Seite verwendet »Kopieren beim Schreiben«. Modifiziert (Dirty) Es wurde auf die Seite geschrieben. Global Die Übersetzung gilt für alle Prozesse. Beispielsweise wird dieser PTE nicht davon beeinflusst, wenn ein Übersetzungspuffer auf die Festplatte geleert wird. Große Seite (Large page) Gibt an, dass der PDE eine 2-MB-Seite zuordnet (siehe den Abschnitt »Große und kleine Seiten« weiter vorn in diesem Kapitel). Keine Ausführung (No execute) Gibt an, dass auf dieser Seite kein Code ausgeführt werden kann (sie lässt sich nur für Daten verwenden). Besitzer (Owner) Gibt an, ob Benutzermoduscode auf die Seite zugreifen kann oder ob die Seite auf Kernelmoduszugriff beschränkt ist. Prototyp Der PTE ist ein Prototyp, der als Vorlage zur Beschreibung von gemeinsam genutztem Speicher in Verbindung mit Abschnittsobjekten verwendet wird. Gültig (Valid) Gibt an, ob die Übersetzung die Seite im physischen Speicher zuordnet. → Adressübersetzung Kapitel 5 427
Bit Bedeutung Write-Through Kennzeichnet die Seite für Write-Through oder (falls der Prozessor die Seitenattributtabelle unterstützt) für kombiniertes Schreiben. Dies wird gewöhnlich für die Zuordnung von Arbeitsspeicher eines Videoframepuffers verwendet. Schreiben (Write) Teilt der MMU mit, ob auf die Seite geschrieben werden kann. Tabelle 5–10 Status- und Schutzbits in einem PTE Hardware-PTEs auf x86-Systemen enthalten zwei Bits, die die MMU ändern kann, nämlich »Modifiziert« und »Zugriff erfolgt«. Die MMU setzt das Zugriffsbit, wenn eine Seite gelesen oder auf ihr geschrieben wurde (und das Bit noch nicht gesetzt ist). Das Modifizierungsbit setzt sie, wenn eine Schreiboperation auf der Seite erfolgt. Das Betriebssystem ist dafür zuständig, diese Bits zur richtigen Zeit wieder aufzuheben. Die MMU hebt sie niemals wieder auf. Für den Seitenschutz verwendet die x86-MMU ein Schreibbit. Ist es nicht gesetzt, so kann die Seite nur gelesen werden, anderenfalls gelesen und geschrieben. Versucht ein Thread, auf eine Seite ohne Schreibbit zu schreiben, tritt eine Speicherverwaltungsausnahme auf. Der Zugriffsfehlerhandler des Speicher-Managers (siehe den Abschnitt »Seitenfehler« weiter hinten) muss bestimmen, ob der Thread die Erlaubnis zum Schreiben auf der Seite erhalten kann (z. B. wenn die Seite für das Kopieren beim Schreiben gekennzeichnet ist) oder ob eine Zugriffsverletzung gemeldet werden soll. Hardware- und Softwareschreibbits in Seitentabelleneinträgen Das zusätzliche, in der Software implementierte Schreibbit (siehe Abb. 5–18) dient dazu, die Aktualisierung des Modifizierungsbits mit den Aktualisierungen der Windows-Speicherverwaltungsdaten zu synchronisieren. Im einfachsten Fall setzt der Speicher-Manager das Hardware-Schreibbit (Bit 1) für jede schreibbare Seite. Ein Schreibvorgang auf einer solchen Seite führt dazu, dass die MMU das Modifizierungsbit im PTE setzt. Später teilt dieses Modifizierungsbit dem Speicher-Manager mit, dass der Inhalt der physischen Seite in den Hintergrundspeicher geschrieben werden muss, bevor die physische Seite für etwas anderes verwendet werden kann. In der Praxis kann dies auf Mehrprozessorsystemen jedoch zu Race Conditions führen, die sich nur mit großem Aufwand aufheben lassen. Die MMUs der einzelnen Prozessoren können jederzeit das Modifizierungsbit eines beliebigen PTEs setzen, dessen Hardware-Schreibbit gesetzt ist. Der Speicher-Manager muss die Arbeitssatzliste des Prozesses zu verschiedenen Zeiten aktualisieren, damit sie den Zustand des Modifizierungsbits im PTE widerspiegeln. Um den Zugriff auf diese Liste zu synchronisieren, verwendet der Speicher-Manager ein Pushlock. Doch auf Mehrprozessorsystemen kann das Modifizierungsbit auch dann von den MMUs anderer CPUs geändert werden, wenn ein Prozessor eine Sperre aufrechterhält. Dadurch ist es möglich, dass eine Aktualisierung eines Modifizierungsbits verloren geht. Um das zu vermeiden, initialisiert der Speicher-Manager von Windows sowohl schreibgeschützte als auch schreibbare Seiten so, dass das Hardware-Schreibbit (Bit 1) ihrer PTEs auf 0 gesetzt ist, und merkt sich die tatsächliche Schreibfähigkeit der Seite mit dem Software-Schreibbit (Bit 11). Beim ersten Schreibzugriff auf eine solche Seite löst der Prozessor eine Speicherverwal- 428 Kapitel 5 Speicherverwaltung
tungsausnahme auf, da das Hardware-Schreibbit nicht gesetzt ist, so wie er es auch bei einer echten schreibgeschützten Seite tut. In diesem Fall aber kann er über das Software-Schreibbit erkennen, dass die Seite tatsächlich schreibbar ist, weshalb er ein Pushlock für den Arbeitssatz erwirbt, das Modifizierungsbit und das Hardware-Schreibbit im PTE setzt, die Arbeitssatzliste mit dem Hinweis auf die Änderung der Seite aktualisiert, das Pushlock freigibt und die Ausnahme verwirft. Die Hardware-Schreiboperation wird dann wie gewöhnlich fortgesetzt, aber das Modifizierungsbit konnte gesetzt werden, während das Pushlock für die Arbeitssatzliste bestand. Bei nachfolgenden Schreibvorgängen auf der Seite treten keine Ausnahmen mehr auf, da das Hardware-Schreibbit jetzt gesetzt ist. Die MMU setzt weiterhin das Modifizierungsbit, was zwar überflüssig ist, aber nicht schadet, da in der Arbeitssatzliste bereits festgehalten ist, dass auf die Seite geschrieben wurde. Es mag nach übermäßigem Mehraufwand aussehen, dass der erste Schreibvorgang auf einer Seite die Ausnahmebehandlung durchlaufen muss, doch das geschieht für jede schreibbare Seite nur einmal, solange sie gültig bleibt. Außerdem durchläuft der erste Zugriff auf fast jede Seite ohnehin die Ausnahmebehandlung für die Speicherverwaltung, da die Seiten gewöhnlich im ungültigen Zustand initialisiert sind (das PTE-Bit 0 ist nicht gesetzt). Wenn dieser erste Zugriff auch gleich der erste Schreibvorgang auf der Seite ist, erfolgt die hier beschriebene Handhabung des Modifizierungsbits während der Ausnahmebehandlung für den Seitenfehler beim ersten Zugriff, weshalb nur wenig Zusatzaufwand entsteht. Außerdem erlaubt diese Implementierung sowohl auf Einzel- als auch auf Mehrprozessorsystemen, den Look-Aside-Übersetzungspuffer auf die Festplatte zu leeren, ohne dass dabei für jede betroffene Seite eine Sperre erforderlich wäre. Der Look-Aside-Übersetzungspuffer für die Übersetzung Wie Sie bereits wissen, sind für jede Hardwareadressübersetzung drei Nachschlagevorgänge erforderlich: QQ Suche nach dem passenden Eintrag in der PDPT QQ Suche nach dem passenden Eintrag im Seitenverzeichnis (der den Speicherort der Seitentabelle ergibt) QQ Suche nach dem passenden Eintrag in der Seitentabelle Diese drei zusätzlichen Nachschlagevorgänge für jeden Verweis auf eine virtuelle Adresse würden die erforderliche Bandbreite zum Arbeitsspeicher vervierfachen und damit die Leistung beeinträchtigen. Daher halten alle CPUs Adressübersetzungen im Cache fest, sodass Adressen, auf die wiederholt zugegriffen wird, nicht immer wieder neu übersetzt werden müssen. Dieser Cache ist ein assoziatives Speicherarray und wird als Look-Aside-Übersetzungspuffer (Translation Look-Aside Buffer, TLB) bezeichnet. Bei assoziativem Speicher handelt es sich um einen Vektor, dessen Elemente gleichzeitig gelesen und mit einem Zielwert verglichen werden können. Im Fall des TLBs enthält dieser Vektor die kürzlich ermittelten Zuordnungen von virtuellen zu physischen Seiten (siehe Abb. 5–19) sowie die Art des Seitenschutzes, die Größe, Attribute usw. der einzelnen Seiten. Jeder Eintrag im TLB besteht aus einem Tag, das Teile der virtuellen Adresse enthält, und den zugehörigen Daten, nämlich der Nummer der physischen Seite, dem Adressübersetzung Kapitel 5 429
Schutzfeld, dem Gültigkeitsbit und gewöhnlich auch einem Modifizierungsbit. Ist das Globalbit des zugehörigen PTEs gesetzt (wie es in Windows bei Systemraumseiten der Fall ist, die für alle Prozesse sichtbar sind), wird der TLB-Eintrag bei Prozesskontextwechseln nicht ungültig gemacht. Nummer der virtuellen Seite: 17 Virtuelle Seite 5 Seitenrahmen 288 Virtuelle Seite 65 Ungültig Virtuelle Seite 17 Seitenrahmen 1652 Virtuelle Seite 57 Ungültig Virtuelle Seite 11 Seitenrahmen 544 Virtuelle Seite 445 Seitenrahmen 60 Gleichzeitiges Lesen und Vergleichen Abbildung 5–19 Zugriff auf den TLB Für häufig verwendete virtuelle Adressen gibt es sehr wahrscheinlich einen Eintrag im TLB, was eine äußerst schnelle Übersetzung und damit einen schnellen Speicherzugriff ermöglicht. Ist eine virtuelle Adresse nicht im TLB verzeichnet, kann sie sich nach wie vor im Arbeitsspeicher befinden, allerdings sind dann mehrere Speicherzugriffe erforderlich, um sie zu finden, was den Zugriff etwas verlangsamt. Wurde eine virtuelle Seite ausgelagert oder hat der Speicher-Manager den PTE geändert, so muss der Speicher-Manager den TLB-Eintrag ausdrücklich ungültig machen. Greift ein Prozess erneut auf die Seite zu, tritt ein Seitenfehler auf, weshalb der Speicher-Manager die Seite (falls nötig) wieder in den Arbeitsspeicher zurückholt und den PTE neu erstellt. Dadurch kommt auch wieder ein Eintrag im TLB zustande. Experiment: Adressen übersetzen Um zu verdeutlichen, wie die Adressübersetzung funktioniert, sehen wir uns diesen Vorgang auf einem x86-PAE-System an. Mit den Werkzeugen, die im Kerneldebugger bereitstehen, untersuchen wir dabei die PDPT, die Seitenverzeichnisse, Seitentabellen und PTEs. Hier verwenden wir einen Prozess mit der virtuellen Adresse 0x3166004, die zurzeit einer gültigen physischen Adresse zugeordnet ist. In späteren Experimenten schauen wir uns an, was bei den ungültigen Adressen geschieht. → 430 Kapitel 5 Speicherverwaltung
Als Erstes rechnen wir die Adresse 0x3166004 ins Binärformat um, was 11.0001.0110.0 110.0000.0000.0100 ergibt. Anschließend zerlegen wir diese Adresse in die drei Felder, die zur Übersetzung verwendet werden: Index des Seiten­verzeichnis­ Seiten­ index (24) verzeichnis­ zeigers Seitentabellenindex (0x166 or 358) Byteoffset (4) Um mit der Übersetzung beginnen zu können, muss die CPU die physische Adresse der PDPT des laufenden Prozesses kennen. Diese Adresse steht im Register CR3. In der Ausgabe des Befehls !process finden Sie sie im Feld DirBase: lkd> !process -1 0 PROCESS 99aa3040 SessionId: 2 Cid: 1690 Peb: 03159000 ParentCid: 0920 DirBase: 01024800 ObjectTable: b3b386c0 HandleCount: <Data Not Accessible> Image: windbg.exe Die PDPT befindet sich also an der physischen Adresse 0x1024800. Wie Sie der vorhergehenden Abbildung entnehmen können, hat das PDPT-Indexfeld der virtuellen Adresse in unserem Beispiel den Wert 0. Daher steht die physische Adresse des entsprechenden Seitenverzeichnisses im ersten Eintrag der PDPT, also ebenfalls an der physischen Adresse 0x1024800. Der Kerneldebuggerbefehl !pte zeigt den PDE und den PTE für die virtuelle Adresse an: lkd> !pte 3166004 VA 03166004 PDE at C06000C0 PTE at C0018B30 contains 0000000056238867 contains 800000005DE61867 pfn 56238 pfn 5de61 ---DA--UWEV ---DA--UW-V Die PDPT gibt der Debugger dabei nicht aus, aber Sie können sie sich anzeigen lassen, indem Sie ihre physische Adresse übergeben: lkd> !dq 01024800 L4 # 1024800 00000000'53c88801 00000000'53c89801 # 1024810 00000000'53c8a801 00000000'53c8d801 Hier haben wir den Debugger-Erweiterungsbefehl !dq verwendet. Er ähnelt dem Befehl dq (Anzeige in Form von Quadwords, also 64-Bit-Werten), ermöglicht es uns aber, den Arbeitsspeicher anhand von physischen statt virtueller Adressen zu untersuchen. Da wir wissen, dass die PDPT nur vier Einträge umfasst, haben wir das Längenargument L 4 angegeben, um die Ausgabe übersichtlich zu halten. → Adressübersetzung Kapitel 5 431
Da der PDPT-Index (die beiden signifikantesten Bits) der Beispieladresse 0 ist, handelt es sich bei dem gewünschten PDPT-Eintrag um das erste angezeigte Quadword. PDPT-Einträge haben ein ähnliches Format wie PDEs und PTEs. Wie Sie hier sehen, enthält dieser die PFN 0x53c88 (immer ausgerichtet an den Seitengrenzen) für die physische Adresse 0x53c88000. Das ist die physische Adresse des Seitenverzeichnisses. Die Ausgabe von !pte zeigt die PDE-Adresse 0xC06000C0, allerdings als virtuelle und nicht als physische Adresse. Auf x86-Systemen beginnt das Seitenverzeichnis des ersten Prozesses bei der virtuellen Adresse 0xC0600000. In diesem Fall liegt die PDE-Adresse 0xC0, also 24 Mal 8 Bytes (die Größe eines Eintrags) hinter der Startadresse des Seitenverzeichnisses. Der Seitenverzeichnisindex der Beispieladresse lautet also 24, weshalb wir uns den 25. PDE im Seitenverzeichnis ansehen müssen. Der PDE gibt die PFN der gewünschten Seitentabelle an. In diesem Beispiel lautet sie 0x56238, was bedeutet, dass die Seitentabelle an der physischen Adresse 0x56238000 beginnt. Die MMU multipliziert den Seitentabellenindex aus der virtuellen Adresse (0x166) mit 8 (der Größe eines PTEs in Byte) und addiert das Ergebnis zu der Startadresse. Dadurch ergibt sich als physische Adresse des PTEs 0x56238B30. Der Debugger zeigt, dass sich der PTE an der virtuellen Adresse 0xC0018B30 befindet. Der Byteoffset (0xB30) ist der gleiche wie für die physische Adresse, was bei der Adressübersetzung immer der Fall ist. Da der Speicher-Manager Seitentabellen beginnend mit 0xC0000000 zuordnet, ergibt die Addition von 0xB30 zu 0xC0018000 (die 0x18 steht für den Eintrag 24) die in der Ausgabe des Kerneldebuggers gezeigte virtuelle Adresse von 0xC0018B30. Das PFN-Feld des PTEs enthält, wie der Debugger ebenfalls zeigt, den Wert 0x5DE61. Als Letztes müssen wir noch den Byteoffset in der ursprünglichen Adresse berücksichtigen. Wie bereits beschrieben, verkettet die MMU den Byteoffset mit der PFN aus dem PTE, was die physische Adresse 0x5DE61004 ergibt. Sie entspricht der ursprünglichen virtuellen Adresse 0x3166004 – zumindest zum jetzigen Zeitpunkt. Rechts neben der PFN stehen im PTE auch noch Flagbits, in unserem Beispiel ---DA-UW-V. Das A steht hier für »accessed« (»Zugriff erfolgt«; die Seite wurde also gelesen), U für »user-mode accessible« (»vom Benutzermodus aus zugänglich«, also nicht nur vom Kernelmodus aus), W für »writable« (»schreibbar«, also nicht nur lesbar) und V für »valid« (»gültig«; der PTE beschreibt eine gültige Seite im physischen Speicher). Um die Berechnung der physischen Adresse zu bestätigen, schauen Sie sich den fraglichen Speicher sowohl über seine virtuelle als auch über seine physische Adresse an. Als Erstes wenden Sie den Debuggerbefehl dd (DWORD-Anzeige) auf die virtuelle Adresse an: lkd> dd 3166004 L 10 03166004 00000034 00000006 00003020 0000004e 03166014 00000000 00020020 0000a000 00000014 → 432 Kapitel 5 Speicherverwaltung
Wenn Sie !dd auf die berechnete physische Adresse anwenden, sehen Sie denselben Inhalt: lkd> !dd 5DE61004 L 10 #5DE61004 00000034 00000006 00003020 0000004e #5DE61014 00000000 00020020 0000a000 00000014 Auf diese Weise können Sie auch die Inhalte an den virtuellen und physischen Adressen des PTEs und des PDEs vergleichen. Übersetzung virtueller Adressen auf x64-Systemen Die Adressübersetzung auf x64-Systemen ähnelt der auf x86-Systemen, allerdings kommt noch eine vierte Ebene hinzu. Jeder Prozess verfügt noch über ein zusätzliches primäres Seitenverzeichnis, die Seitenzuordnungstabelle vierter Ebene, die die physischen Speicherorte der 512 Strukturen dritter Ebene enthält, also der sogenannten Seitenverzeichniszeiger. Diese Seitenverzeichniszeiger entsprechen der PDPT auf x86-PAE-Systemen, allerdings gibt es 512 davon und nicht nur einen, und jeder ist eine komplette Seite mit 512 statt lediglich vier Einträgen. Wie die Einträge der PDPT enthalten auch diejenigen der Seitenverzeichniszeiger die physischen Speicherorte der einzelnen Seitentabellen. Die Seitentabellen wiederum enthalten jeweils 512 Seitentabelleneinträge mit den physischen Speicherorten der Seiten. Alle »physischen Speicher­ orte« sind in diesen Strukturen in Form von PFNs gespeichert. Bei den aktuellen Implementierungen der x64-Architektur sind virtuelle Adressen auf 48 Bits beschränkt. Die Bestandteile solcher virtuellen 48-Bit-Adressen und ihre Verwendung für die Adressübersetzung sehen Sie in Abb. 5–20. Den Aufbau eines x64-Hardware-PTEs sehen Sie in Abb. 5–21. Seitenzuordnung Ebene 4 Seiten­ zuordnung Ebene 4 Seitenverzeichniszeiger Seiten­ verzeichnis­ zeiger Seitenverzeichnis Seiten­ verzeichnisse Seitentabelle Byte auf der Seite Seiten­tabellen Abbildung 5–20 x64-Adressübersetzung Adressübersetzung Kapitel 5 433
Modifiziert Große Seite Zugriff erfolgt Global Cache deaktiviert Kopieren beim Schreiben (Software) Write-Through Prototyp (Software) Besitzer Schreiben (Software) Schreiben Keine Ausführung Gültig Reserviert Abbildung 5–21 Aufbau eines x64-Hardware-PTEs Übersetzung virtueller Adressen auf ARM-Systemen Für die Übersetzung virtueller Adressen auf 32-Bit-ARM-Prozessoren wird ein einziges Seitenverzeichnis mit 1024 Einträgen von je 32 Bits verwendet. Die für die Übersetzung herangezogenen Strukturen sehen Sie in Abb. 5–22. 1024 Einträge 1024 Einträge Seite Byte auf der Seite Seitenverzeichnis (eines pro Prozess) Seitentabelle(n) Abbildung 5–22 Strukturen zur Übersetzung virtueller Adressen auf ARM-Systemen Jeder Prozess verfügt über ein einziges Seitenverzeichnis, dessen physische Adresse im Register TTBR gespeichert ist (vergleichbar mit dem Register CR3 auf x86/x64-Systemen). Die zehn signifikantesten Bits der virtuellen Adresse bezeichnen einen PDE, der auf eine der 1024 Seitentabellen zeigt. Die nächsten zehn Bits der virtuellen Adresse geben einen bestimmten PTE an. Jeder gültige PTE zeigt auf den Anfang einer Seite im physischen Arbeitsspeicher, und der gewünschte Offset wird von den unteren 12 Bits der Adresse festgelegt (wie auf x86und x64-Systemen). Das Schema in Abb. 5–22 deutet schon an, dass der adressierbare physische Speicher 4 GB umfasst, da die einzelnen PTEs kleiner sind als bei x86/x64 (32 statt 64 Bits). Tatsächlich werden für die PFN nur 20 Bits verwendet. ARM-Prozessoren unterstützen auch einen PAE-Modus (ähnlich dem x86-PAE-Modus), doch Windows nutzt diese Möglichkeit nicht. 434 Kapitel 5 Speicherverwaltung
In zukünftigen Windows-Versionen mag eine Unterstützung für die 64-Bit-ARM-Architektur hinzukommen, was die Einschränkungen bei den physischen Adressen verringern und den virtuellen Adressraum für Prozesse und das System dramatisch vergrößern wird. Kurioserweise unterscheiden sich PTEs, PDEs und PDEs für große Seiten in ihrem Aufbau voneinander. Abb. 5–23 zeigt den Aufbau eines gültigen PTEs für ARMv7, wie es zurzeit von Windows verwendet wird. Weitere Informationen entnehmen Sie bitte der offiziellen ARM-Dokumentation. Schreibbar (Software) Kopieren beim Schreiben (Software) Typerweiterung (stets 0) Besitzer Nicht modifiziert Zugriff erfolgt Große Seite (Software) Cachetyp Nicht global Gültig (2 Bits) Abbildung 5–23 Aufbau eines gültigen ARM-PTEs Seitenfehler Wie die Adressübersetzung bei gültigen PTEs funktioniert, wissen Sie nun bereits. Ist das Gültigkeitsbit aber nicht gesetzt, bedeutet das, dass die gewünschte Seite für den Prozess zurzeit aus irgendeinem Grund nicht zugänglich ist. In diesem Abschnitt geht es um die verschiedenen Arten von ungültigen PTEs und die Auflösung von solchen ungültigen Verweisen. In diesem Abschnitt beschreiben wir nur PTEs auf x86-Systemen. Die PTEs auf 64Bit- und ARM-Systemen enthalten ähnliche Informationen, allerdings werden wir ihren Aufbau hier nicht ausführlich erläutern. Ein Verweis auf eine ungültige Seite wird als Seitenfehler bezeichnet. Der Kerneltraphandler (siehe den Abschnitt »Trapdisponierung« in Kapitel 8 von Band 2) übergibt Fehler dieser Art an den Fehlerhandler MmAccessFault des Speicher-Managers. Diese Routine wird im Kontext des Threads ausgeführt, in dem der Fehler aufgetreten ist. Sie muss versuchen, den Fehler zu lösen, und wenn das nicht möglich ist, eine entsprechende Ausnahme auslösen. Es gibt eine Vielzahl von Bedingungen, die solche Fehler verursachen können (siehe Tabelle 5–11). Seitenfehler Kapitel 5 435
Ursache Folge Beschädigter PTE/PDE Beendigungsfehler (Absturz) des Systems mit dem Code 0x1A (MEMORY_MANAGEMENT) Zugriff auf eine Seite, die sich nicht im Speicher, sondern in einer Auslagerungsdatei auf der Festplatte oder in einer im Speicher zugeordneten Datei befindet Es wird eine physische Seite zugewiesen und die gewünschte Seite von der Festplatte gelesen und in den entsprechenden Arbeitssatz gestellt. Zugriff auf eine Seite, die sich auf der Liste der Standby- oder modifizierten Seiten befindet Die Seite wird in den geeigneten Arbeitssatz des Prozesses, der Sitzung oder des Systems überführt. Zugriff auf eine Seite, die nicht zugesichert ist (z. B. im reservierten oder nicht zugewiesenen Adressraum) Ausnahme aufgrund einer Zugriffsverletzung Benutzermoduszugriff auf eine Seite, die nur vom Kernelmodus aus zugänglich ist Ausnahme aufgrund einer Zugriffsverletzung Schreiben auf eine schreibgeschützte Seite Ausnahme aufgrund einer Zugriffsverletzung Zugriff auf eine Nullforderungsseite Dem entsprechenden Arbeitssatz wird eine mit Nullen gefüllte Seite hinzugefügt. Schreiben auf eine Wächterseite Wächterseitenverletzung (falls ein Verweis auf einen Benutzermodusstack vorhanden ist, wird eine automatische Stackerweiterung durchgeführt) Schreiben auf eine Copy-on-Write-Seite (Kopieren beim Schreiben) Es wird eine private Kopie der Seite für den Prozess oder die Sitzung erstellt. Damit wird die Originalseite im Arbeitssatz des Prozesses, der Sitzung oder des Systems ersetzt. Schreiben auf eine Seite, die zwar gültig ist, aber noch nicht in die aktuelle Kopie im Hintergrundspeicher geschrieben wurde Das Modifizierungsbit des PTEs wird gesetzt. Ausführen von Code auf einer als nicht ausführbar gekennzeichneten Seite Ausnahme aufgrund einer Zugriffsverletzung Die PTE-Berechtigungen entsprechen nicht den Enklavenberechtigungen (siehe den Abschnitt »Speicherenklaven« weiter hinten in diesem ­Kapitel und die Dokumentation der Funktion ­CreateEnclave im Windows SDK) • Benutzermodus: Ausnahme aufgrund einer ­Zugriffsverletzung • Kernelmodus: Beendigungsfehler mit Code 0x50 (PAGE_FAULT_IN_NONPAGED_AREA) Tabelle 5–11 Gründe für Zugriffsfehler In den folgenden Abschnitten sehen wir uns zunächst die vier grundlegenden Arten von ungültigen PTEs an, die der Zugriffsfehlerhandler verarbeitet, und dann den Sonderfall der Prototyp-PTEs, die zur Einrichtung von gemeinsam nutzbaren Seiten verwendet werden. Ungültige PTEs Wenn das Gültigkeitsbit eines PTEs bei der Adressübersetzung 0 ist, dann repräsentiert dieser PTE eine ungültige Seite. Bei einem Verweis darauf wird eine Speicherverwaltungsausnahme oder ein Seitenfehler ausgelöst. Die MMU ignoriert dann die restlichen Bits des PTEs, sodass das Betriebssystem darin Informationen über die Seite speichern kann, die benötigt werden, um den Seitenfehler zu beheben. 436 Kapitel 5 Speicherverwaltung
In der folgenden Aufstellung finden Sie die fünf Arten von ungültigen PTEs und ihren Aufbau. Sie werden oft auch als Software-PTEs bezeichnet, da sie vom Speicher-Manager statt von der MMU interpretiert werden. Einige ihrer Flags sind identisch mit denen für Hardware-PTEs, die Sie in Tabelle 5–10 gesehen haben, und auch einige der Bitfelder haben die gleiche oder eine ähnliche Bedeutung wie die entsprechenden Felder in einem Hardware-PTE. QQ Auslagerungsdatei Die gewünschte Seite befindet sich in einer Auslagerungsdatei. Wie Abb. 5–24 zeigt, geben vier Bits im PTE an, in welcher der 16 möglichen Auslagerungsdateien die Seite steht, während weitere 32 Bits die Nummer der Seite in der Datei nennen. Der Pager – also der Auslagerungsmechanismus – löst eine Einlagerungsoperation aus, um die Seite wieder in den Arbeitsspeicher zu holen und gültig zu machen. Der Offset in der Auslagerungsdatei ist immer von 0 verschieden und besteht niemals komplett aus Einsen (die erste und die letzte Seite in der Auslagerungsdatei werden nicht zur Auslagerung verwendet). Dadurch sind die im Folgenden beschriebenen anderen Formate möglich. In der Auslagerungsdatei reserviert Übergang In der Auslagerungsdatei zugewiesen Offset in Auslagerungsdatei Nicht verwendet (32 Bits) (18 Bits) Gültig Prototyp Schutz Index der Auslagerungsdatei Abbildung 5–24 Ein PTE für eine Seite in einer Auslagerungsdatei QQ Nullforderung Dieses PTE-Format ist mit dem für Seiten in einer Auslagerungsdatei identisch, allerdings wird das Feld mit dem Offset in der Auslagerungsdatei auf 0 gesetzt. Die gewünschte Seite muss in Form einer Seite bereitgestellt werden, die nur aus Nullen besteht. Der Pager untersucht dazu die Nullseitenliste. Ist sie leer, entnimmt der Pager eine Seite von der Liste der freien Seiten und setzt sie auf 0. Ist auch diese Liste leer, nimmt er die Seite von den Standbylisten und setzt sie auf 0. QQ VAD (Virtual Address Descriptor) Dieses PTE-Format ist mit dem für Seiten in einer Auslagerungsdatei identisch, allerdings wird das Feld mit dem Offset in der Auslagerungsdatei komplett auf 1 gesetzt. Das steht für eine Seite, deren Definition und ggf. Hintergrundspeicher im VAD-Baum des Prozesses zu finden sind. Dieses Format wird für Seiten verwendet, hinter denen Abschnitte in zugeordneten Dateien stehen. Der Pager findet den VAD, der den virtuellen Adressbereich mit der virtuellen Seite definiert, und löst einen Einlagerungsvorgang von der in dem VAD genannten zugeordneten Datei aus. (VADs werden in dem gleichnamigen Abschnitt weiter hinten in diesem Kapitel ausführlicher beschrieben.) QQ Übergang Das Übergangsbit ist 1. Die gewünschte Seite befindet sich zwar im Arbeitsspeicher, aber entweder auf der Standbyliste, der Liste geänderter Seiten, der Liste geänderter und nicht schreibbarer Seiten oder auf gar keiner Liste. Der Pager nimmt die Seite ggf. von der Liste und fügt sie dem Arbeitssatz des Prozesses hinzu. Da keine Ein-/Ausgabe erforderlich ist, wird dies auch als weicher Seitenfehler bezeichnet. QQ Unbekannt Der PTE ist 0 oder die Seitentabelle existiert nicht (wobei der PDE mit der physischen Adresse der Seitentabelle 0 ist). In beiden Fällen muss der Speicher-Manager Seitenfehler Kapitel 5 437
die VADs untersuchen, um zu bestimmen, ob diese virtuelle Adresse zugesichert wurde. Wenn ja, werden Seitentabellen für den neu zugesicherten Adressraum erstellt. Ist anderenfalls die Seite reserviert oder gar nicht definiert, wird der Seitenfehler als Ausnahme aufgrund einer Zugriffsverletzung gemeldet. Prototyp-PTEs Wenn eine Seite von zwei Prozessen gemeinsam genutzt werden kann, ordnet der Speicher-Manager sie mithilfe von sogenannten Prototyp-PTEs zu. Für auslagerungsgepufferte Abschnitte wird beim erstmaligen Erstellen eines Abschnittsobjekts ein Array aus Prototyp-PTEs angelegt, und für zugeordnete Dateien werden bei der Zuordnung der einzelnen Sichten Teile des Arrays nach Bedarf erstellt. Die Prototyp-PTEs gehören zu der Segmentstruktur (die weiter hinten in diesem Kapitel im Abschnitt »Abschnittsobjekte« genauer beschrieben wird). Wenn ein Prozess zum ersten Mal auf eine Seite verweist, die einer Sicht eines Abschnittsobjekts zugeordnet ist (denken Sie daran, dass VADs nur bei der Zuordnung der Sicht erstellt werden!), nutzt der Speicher-Manager die Informationen im Prototyp-PTE, um den eigentlichen, für die Adressübersetzung verwendeten PTE in der Seitentabelle des Prozesses auszufüllen. Wird eine gemeinsam genutzte Seite gültig gemacht, so zeigen sowohl der Prozess- als auch der Prototyp-PTE auf dieselbe physische Seite mit den Daten. Um die Anzahl der Prozess-PTEs im Auge zu behalten, die auf eine gültige gemeinsame Seite verweisen, wird ein Zähler in deren PFN-Datenbankeintrag geführt. Der Speicher-Manager kann daher erkennen, wenn es für eine gemeinsam genutzte Seite keine Verweise mehr in irgendeiner Seitentabelle gibt, sodass er sie ungültig machen und auf die Übergangsliste setzen oder auf die Festplatte schreiben kann. Wird eine gemeinsam nutzbare Seite ungültig gemacht, so wird der PTE in der Prozessseitentabelle durch einen besonderen PTE ersetzt, der auf den Prototyp-PTE mit der Beschreibung der Seite zeigt (siehe Abb. 5–25). Bei einem Zugriff auf die Seite kann der Speicher-Manager daher diesen Prototyp-PTE finden. Kombiniert Nur Lesezugriff Gültig Prototyp verwendet Adresse des Prototyp-PTEs Nicht(16 Bits) Schutz Nicht verwendet (5 Bits) Abbildung 5–25 Aufbau eines ungültigen PTEs mit einem Zeiger auf den Prototyp-PTE Der Prototyp-PTE gibt an, in welchem der sechs möglichen Zustände sich eine gemeinsam genutzte Seite befindet: QQ Aktiv/gültig (Active) Die Seite befindet sich im physischen Speicher, da ein anderer Prozess darauf zugegriffen hat. QQ Übergang (Transition) Die gewünschte Seite befindet sich im Arbeitsspeicher, steht aber auf der Standbyliste oder der Liste der geänderten Seiten (oder auf gar keiner Liste). 438 Kapitel 5 Speicherverwaltung
QQ Geändert, nicht schreibbar (Modified no-write) Die gewünschte Seite befindet sich im Arbeitsspeicher und auf der Liste der geänderten und nicht schreibbaren Seiten. QQ Nullforderung (Demand zero) werden. Für die gewünschte Seite soll eine aus Nullen verwendet QQ Auslagerungsdatei (Page file) Die gewünschte Seite befindet sich in der Auslagerungsdatei. QQ Zugeordnete Datei (Mapped file) ordneten Datei. Die gewünschte Datei befindet sich in einer zuge- Prototyp-PTEs weisen den gleichen Aufbau auf wie die zuvor beschriebenen echten PTEs, werden aber nicht für die Adressübersetzung verwendet. Sie bilden eine Schicht zwischen den Seitentabellen und der PFN-Datenbank und erscheinen niemals direkt in Seitentabellen. Der Speicher-Manager kann die gemeinsamen Seiten verwalten, ohne die Seitentabellen der einzelnen Prozesse aktualisieren zu müssen, die diese Seiten nutzen, da alle zugreifenden Prozesse zur Auflösung von Fehlern auf einen Prototyp-PTE verweisen werden. Wenn der Speicher-Manager eine gemeinsam genutzte Code- oder Datenseite abruft, die auf die Festplatte ausgelagert wurde, muss er nur den Prototyp-PTE aktualisieren, der auf ihren neuen physischen Standort zeigt. Die PTEs in den einzelnen Prozessen, die die Seite nutzen, bleiben unverändert. Ihr Gültigkeitsbit ist nicht gesetzt, und sie zeigen auf den Prototyp. Wenn später Prozesse auf die Seite verweisen, wird der echte PTE aktualisiert. Abb. 5–26 zeigt zwei virtuelle Seiten in einer zugeordneten Sicht. Die erste Seite ist gültig, und sowohl der Prozess- als auch der Prototyp-PTE zeigen auf sie. Die zweite Seite dagegen befindet sich in der Auslagerungsdatei. Während der Prototyp-PTE den genauen Speicherort angibt, zeigt der PTE des Prozesses (sowie aller anderen Prozesse, die diese Seite verwenden) auf den Prototyp-PTE. PFN 5 PFN 5 Gültig – PFN 5 Ungültig – zeigt auf den Prototyp-PTE PTE-Adresse Gültig – PFN 5 Zähler für gemeinsame Nutzung = 1 Ungültig – in der Auslagerungsdatei PFN-Datenbankeintrag Physischer Speicher Seitentabelle Prototyp-Seitentabelle Abbildung 5–26 Prototyp-PTEs Seitenfehler Kapitel 5 439
Einlagerungs-E/A Einlagerungs-E/A tritt auf, wenn eine Auslagerungs- oder zugeordnete Datei gelesen werden muss, um einen Seitenfehler aufzulösen. Da auch Seitentabellen ausgelagert werden können, ist es möglich, dass bei der Verarbeitung eines Seitenfehlers zusätzliche Ein-/Ausgaben erforderlich sind, falls das System erst die Seite mit der erforderlichen Seitentabelle laden muss. Einlagerungsoperationen erfolgen synchron, das heißt, der Thread wartet, bis der Vorgang abgeschlossen ist. Sie können auch nicht durch asynchrone Prozeduraufrufe (APCs) unterbrochen werden. Der Pager verwendet in der E/A-Anforderungsfunktion einen besonderen Modifizierer, um anzugeben, dass eine Einlagerungs-E/A erfolgt. Nach Abschluss des Vorgangs löst das E/A-System ein Ereignis aus, das den Pager aufweckt, sodass er mit der Verarbeitung fortfahren kann. Während der Einlagerungsoperation besitzt der Thread, der den Fehler hervorgerufen hat, keine kritischen Objekte zur Synchronisierung der Speicherverwaltung. Andere Threads in demselben Prozess können jedoch Funktionen für den virtuellen Speicher aufrufen und Seitenfehler handhaben, während eine Einlagerung stattfindet. Es gibt jedoch einige besondere Bedingungen, die der Pager bei Abschluss der E/A berücksichtigen muss: QQ Ein anderer Thread in demselben oder einem anderen Prozess hat einen Seitenfehler aufgrund derselben Seite ausgelöst (was als Seitenfehlerkollision bezeichnet und im nächsten Abschnitt erklärt wird). QQ Die Seite kann gelöscht und vom virtuellen Adressraum neu zugeordnet worden sein. QQ Der Schutz der Seite kann sich geändert haben. QQ Der Fehler kann sich auf einen Prototyp-PTE beziehen, wobei es sein kann, dass sich die Seite zur Zuordnung dieses PTEs nicht mehr im Arbeitssatz befindet. Der Pager speichert so viel von dem Status im Kernelstack des Threads vor der Anforderung der Einlagerungs-E/A, dass er diese Bedingungen nach dem Abschluss der Anforderung erkennen und den Seitenfehler ggf. verwerfen kann, ohne die Seite gültig zu machen. Wird die Anweisung, die den Fehler hervorgerufen hat, erneut ausgeführt, so wird der Pager abermals aufgerufen und wertet den PTE in seinem neuen Zustand aus. Seitenfehlerkollisionen Wenn ein anderer Thread desselben oder eines anderen Prozesses einen weiteren Seitenfehler für genau die Seite hervorruft, die gerade eingelagert wird, liegt eine sogenannte Seitenfehlerkollision vor. Auf Multithreadsystemen kommen sie häufig vor, aber der Pager ist in der Lage, sie zu erkennen und zu handhaben. Anhand der Informationen im PFN-Datenbankeintrag kann er feststellen, dass sich die Seite im Übergang befindet und ein Lesevorgang stattfindet. Er kann dann dafür sorgen, dass der zweite Thread auf das in dem PFN-Datenbankeintrag angegebene Ereignis wartet. Dieses Ereignis wurde von dem Thread initialisiert, der als Erster die zur Auflösung des Fehlers nötige E/A ausgelöst hat. Alternativ kann der Pager auch eine parallele E/A veranlassen, um ein Deadlock im Dateisystem zu verhindern. Dabei gewinnt die erste E/A, während die anderen verworfen werden. 440 Kapitel 5 Speicherverwaltung
Nach Abschluss der E/A-Operation können die Threads, die auf das Ereignis gewartet haben, fortfahren. Der erste Thread, der die PFN-Datenbanksperre erworben hat, ist für die Operationen zum Abschluss der Einlagerung verantwortlich. Dazu gehört es, den E/A-Status zu prüfen, um sicherzustellen, dass die E/A-Operation erfolgreich beendet wurde, das Bit für den laufenden Lesevorgang in der PFN-Datenbank zurückzusetzen und den PTE zu aktualisieren. Wenn die nachfolgenden Threads die PFN-Datenbanksperre erwerben, um ihren Seitenfehler zu verarbeiten, erkennt der Pager, dass die ursprüngliche Aktualisierung abgeschlossen wurde, da das Bit für den laufenden Lesevorgang nicht gesetzt ist. Er prüft das Flag für Einlagerungsfehler in dem PFN-Datenbankeintrag, um sich zu vergewissern, dass die Einlagerungs-E/A erfolgreich abgeschlossen wurde. Ist dieses Flag gesetzt, so wurde der PTE nicht aktualisiert, weshalb in dem Thread, der den Fehler ausgelöst hat, eine Ausnahme wegen eines Einlagerungsfehlers (In-Page-Fehler) ausgelöst wird. Seitencluster Der Speicher-Manager ruft vorab große Seitencluster ab, um Seitenfehler verarbeiten zu können und den Systemcache zu füllen. Dabei werden die Daten direkt in den Seitencache des Systems statt in einen Arbeitssatz im virtuellen Speicher geladen. Aus diesem Grund belegen die vorab abgerufenen Daten keinen virtuellen Adressraum, weshalb die Operation auch nicht auf den Umfang des verfügbaren virtuellen Adressraums beschränkt ist. Des Weiteren sind keine teuren Interprozessorinterrupts (IPIs) erforderlich, um den TLB auf die Festplatte zu leeren, wenn die Seiten einem neuen Zweck zugeführt werden müssen. Die vorab abgerufenen Seiten werden auf die Standbyliste gesetzt und im PTE mit »Übergang« markiert. Wird später auf eine solche Seite verwiesen, fügt der Speicher-Manager sie zum Arbeitssatz hinzu. Sollte niemals auf sie verwiesen werden, so werden auch keine Systemressourcen benötigt, um sie freizugeben. Befinden sich Seiten aus dem vorab abgerufenen Cluster bereits im Speicher, liest der Speicher-Manager sie nicht erneut, sondern repräsentiert sie durch eine Platzhalterseite, sodass eine einzige, effiziente, umfangreiche E/A möglich ist (siehe Abb. 5–27). Physischer Speicher Virtueller Adressraum MDL(s) X (ersetzt Y) X (ersetzt Z) Systemweite Platzhalterseite X Die Seiten Y und Z befinden sich bereits im Speicher, weshalb die zugehörigen MDL-Einträge auf die systemweite Platzhalterseite zeigen Abbildung 5–27 Verwendung einer Platzhalterseite bei der Adresszuordnung in einer MDL Seitenfehler Kapitel 5 441
Die Dateioffsets und virtuellen Adressen in der Abbildung, die den Seiten A, Y, Z und B entsprechen, sind logisch zusammenhängend, was aber nicht unbedingt auch für die physischen Seiten gelten muss. Die Seiten A und B sind nicht im Arbeitsspeicher vorhanden, weshalb der Speicher-Manager sie lesen muss. Dagegen befinden sich Y und Z bereits im Arbeitsspeicher, sodass für sie kein Lesevorgang erforderlich ist. (Sie könnten jedoch geändert worden sein, seit sie das letzte Mal aus dem Hintergrundspeicher geladen wurden; in diesem Fall würde ein Überschreiben ihres Inhalts zu einem ernsten Fehler führen.) Es ist jedoch effizienter, die Seiten A und B in einer einzigen Operation zu lesen, weshalb der Speicher-Manager eine Leseopera­ tion ausgibt, die alle vier Seiten (A, Y, Z und B) im Hintergrundspeicher umfasst. Eine solche Anforderung schließt so viele Seiten ein, wie es angesichts der Menge des verfügbaren Speichers, der aktuellen Nutzung des Systems usw. sinnvoll ist. Wenn der Speicher-Manager die MDL zur Beschreibung der Anforderung erstellt, gibt er gültige Zeiger auf die Seiten A und B an. Die Einträge für die Seiten Y und Z jedoch zeigen auf dieselbe systemweite Platzhalterseite X. Diese Seite kann der Speicher-Manager mit den möglicherweise veralteten Daten aus dem Hintergrundspeicher füllen, da er X ohnehin nicht sichtbar macht. Wenn eine Komponente jedoch auf den Y- oder Z-Offset in der MDL zugreift, sieht sie die Platzhalterseite X statt Y bzw. Z. Der Speicher-Manager kann eine beliebige Menge verworfener Seiten durch eine einzige Platzhalterseite darstellen, und diese Seite kann mehrmals in einer einzigen MDL und sogar in mehreren gleichzeitig verwendeten MDLs für unterschiedliche Treiber verwendet werden. Daher können sich auch die Inhalte an den Speicherorten, die für die verworfenen Dateien stehen, jederzeit ändern. (Mehr über MDLs erfahren Sie in Kapitel 6.) Auslagerungsdateien In Auslagerungsdateien werden geänderte Seiten gespeichert, die immer noch von einigen Prozessen verwendet werden, aber auf die Festplatte geschrieben wurden, weil sie nicht zugeordnet waren oder weil der Speicherbedarf dies erforderte. Der Platz in der Auslagerungsdatei wird bereits reserviert, wenn die Seiten zugesichert werden, aber die tatsächlichen, optimal gruppierten Speicherorte in der Auslagerungsdatei werden erst dann ausgewählt, wenn die Seiten auf die Festplatte geschrieben werden. Beim Start des Systems liest der Prozess des System-Managers (Smss.exe) die Liste der zu öffnenden Auslagerungsdateien im Multistring-Registrierungswert HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PagingFiles. Dieser Wert enthält den Namen und die Mindest- und Höchstgröße jeder Auslagerungsdatei. In Windows können auf x86- und x64-Systemen bis zu 16 Auslagerungsdateien verwendet werden und auf ARM-Systemen bis zu zwei. Die Höchstgröße der einzelnen Auslagerungsdateien beträgt 16 TB (x86/x64) bzw. 4 GB (ARM). Sobald eine Auslagerungsdatei geöffnet ist, kann sie nicht wieder geschlossen werden, solange das System läuft, da der Systemprozess Handles für alle Auslagerungsdateien offen hält. Da die Auslagerungsdatei Teile des virtuellen Prozess- und Kernelspeichers enthält, kann das System aus Sicherheitsgründen so eingerichtet werden, dass die Auslagerungsdatei beim Herunterfahren geleert wird. Dazu wird der Registrierungswert ClearPageFileAtShutdown im 442 Kapitel 5 Speicherverwaltung
Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management auf 1 gesetzt. Anderenfalls bleiben in der Auslagerungsdatei nach dem Herunterfahren die Daten enthalten, die aus dem System ausgelagert wurden. Eine Person, die physischen Zugriff auf den Computer hat, kann auf diese Daten zugreifen. Sind die Mindest- und Höchstgröße beide 0 (oder nicht angegeben), handelt es sich um eine vom System verwaltete Auslagerungsdatei. In Windows 7 und Windows Server 2008 R2 wurde dafür ein einfaches Verfahren rein auf der Grundlage der RAM-Größe verwendet: QQ Mindestgröße Wird auf den Umfang des RAM oder auf 1 GB gesetzt, je nachdem welcher Wert größer ist. QQ Höchstgröße Wird auf das Dreifache des RAM oder auf 4 GB gesetzt, je nachdem welcher Wert größer ist. Diese Einstellungen sind jedoch nicht ideal. Der RAM heutiger Laptop- und Desktopcomputer kann 32 oder 64 GB umfassen und auf Servercomputern sogar Hunderte von Gigabyte betragen. Wird die Anfangsgröße der Auslagerungsdatei auf die Größe des RAM gesetzt, kann das zu einem erheblichen Verlust an Festplattenplatz führen, insbesondere bei kleinen Festplatten mit SSD-Technologie. Außerdem ist die Menge des RAM auf einem System nicht unbedingt ein guter Indikator für die typische Speicherlast. Bei der aktuellen Implementierung wird daher ein ausgefeilteres Verfahren verwendet, um eine sinnvolle Mindestgröße für die Auslagerungsdatei zu finden, die nicht allein auf der Größe des RAM beruht, sondern auch die bisherige Nutzung der Auslagerungsdatei und andere Faktoren berücksichtigt. Beim Erstellen und Initialisieren der Auslagerungsdatei berechnet Smss. exe die Mindestgröße auf der Grundlage der folgenden vier in globalen Variablen gespeicherten Faktoren: QQ RAM (SmpDesiredPfSizeBasedOnRAM) rungsdatei auf der Grundlage des RAM. Dies ist die empfohlene Größe der Auslage- QQ Absturzabbild (SmpDesiredPfSizeForCrashDump) Dies ist die erforderliche Größe der Auslagerungsdatei, um ein Absturzabbild zu speichern. QQ Verlauf (SmpDesiredPfSizeBasedOnHistory Dies ist die empfohlene Größe der Auslagerungsdatei auf der Grundlage des Nutzungsverlaufs. Smss.exe verwendet einen Timer, der stündlich eine Aufzeichnung über die Nutzung der Auslagerungsdatei auslöst. QQ Apps (SmpDesiredPfSizeForApps) tei für Windows-Apps. Dies ist die empfohlene Größe der Auslagerungsda- QQ Diese Werte werden wie in Tabelle 5–12 gezeigt berechnet. Seitenfehler Kapitel 5 443
Grundlage Empfohlene Größe der Auslagerungsdatei RAM Bei einem RAM kleiner oder gleich 1 GB wird die Größe auf 1 GB gesetzt. Ist der RAM größer als 1 GB, wird für jedes zusätzliche Gigabyte 1/8 GB bis zu einem Maximum von 32 GB addiert. Absturzabbild Wenn eine dedizierte Datei für das Absturzabbild eingerichtet ist, so wird zur Speicherung keine Auslagerungsdatei verwendet, weshalb die Größe auf 0 gesetzt wird. (Sie können die dedizierte Absturzabbilddatei einrichten, indem Sie dem Schlüssel HKLM\System\CurrentControlSet\Control\CrashControl den Wert DedicatedDumpFile hinzufügen.) Ist als Typ für das Absturzabbild Automatic (der Standardwert) angegeben, gilt: Bei einem RAM von weniger als 4 GB beträgt die Größe 1/6 des RAM. Anderenfalls wird die Größe auf 2/3 GB zuzüglich 1/8 GB für jedes Gigabyte über 4 GB bis zu einem Maximum von 32 GB gesetzt. Ist vor Kurzem ein Absturz erfolgt, bei dem die Auslagerungsdatei nicht groß genug war, dann wird die empfohlene Größe auf den kleineren Wert von der RAM-Größe oder 32 GB erhöht. Ist das System für ein vollständiges Absturzabbild konfiguriert, so wird als Größe der Umfang des RAM zuzüglich des Umfangs der zusätzlichen Informationen in der Absturzabbilddatei zurückgegeben. Ist das System für ein Kernelabsturzabbild konfiguriert, so wird als Größe der Umfang des RAM zurückgegeben. Verlauf Wurden genug Stichproben aufgezeichnet, wird das 90%-Perzentil als empfohlene Größe zurückgegeben, anderenfalls eine Empfehlung auf der Grundlage des vorhandenen RAM (siehe oben). Apps Bei einem Server wird 0 zurückgegeben. Die empfohlene Größe hängt von einem Faktor ab, den der Prozesslebenszyklus-Manager (PLM) verwendet, um zu bestimmen, wann er eine Anwendung beenden soll. Der aktuelle Faktor beträgt das 2,3-Fache des RAM und wurde für RAM-Größen von 1 GB gewählt (was ungefähr das Minimum für Mobilgeräte ist). Auf der Grundlage dieses Faktors wird eine Größe von etwa 2,5 GB berechnet. Wenn das mehr ist als der RAM, wird die RAM-Größe abgezogen. Anderenfalls wird 0 zurückgegeben. Tabelle 5–12 Grundlegende Berechnungen für die Empfehlungen zur Größe der Auslagerungsdatei Die Höchstgröße einer vom System verwalteten Auslagerungsdatei beträgt den größeren Wert von entweder der RAM-Größe oder 4 GB. Die Mindestgröße (ursprüngliche Größe) wird wie folgt bestimmt: QQ Bei der ersten vom System verwalteten Auslagerungsdatei wird die Basisgröße aufgrund des Verlaufs berechnet, anderenfalls auf der Grundlage des RAM (siehe Tabelle 5–12). QQ Bei der ersten vom System verwalteten Auslagerungsdatei wird danach wie folgt vorgegangen: • Ist die Basisgröße kleiner als die berechnete Größe für Anwendungen (SmpDesiredPfSizeForApps), so wird die Basisgröße auf diese Größe gesetzt. • 444 Ist die Basisgröße (danach) kleiner als die Größe für Absturzabbilder (SmpDesiredPfSizeForCrashDump), so wird die Basisgröße auf diese Größe gesetzt. Kapitel 5 Speicherverwaltung
Experiment: Auslagerungsdateien einsehen Eine Liste der Auslagerungsdateien finden Sie im Registrierungswert PagingFiles unter HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management. Dieser Eintrag enthält die Konfigurationseinstellung für die Auslagerungsdatei, die Sie im Dialogfeld Erweiterte Systemeinstellungen anpassen können. Um auf diese Einstellungen zuzugreifen, gehen Sie wie folgt vor: 1. Öffnen Sie die Systemsteuerung. 2. Klicken Sie auf System und Sicherheit und dann auf System. Dadurch wird das Dialogfeld Systemeigenschaften geöffnet, das Sie auch durch einen Rechtsklick auf Computer im Explorer und die Auswahl von Eigenschaften erreichen können. 3. Klicken Sie auf Erweiterte Systemeinstellungen. 4. Klicken Sie im Abschnitt Leistung auf Einstellungen. Dadurch wird das Dialogfeld Leistungsoptionen geöffnet. 5. Klicken Sie auf die Registerkarte Erweitert. 6. Klicken Sie im Abschnitt Virtueller Arbeitsspeicher auf Ändern. Experiment: Die empfohlenen Größen für Auslagerungsdateien einsehen Um sich die in Tabelle 5–12 berechneten Variablen anzusehen, gehen Sie wie folgt vor (hier auf einem x86-System mit Windows 10): 1. Starten Sie den lokalen Kerneldebugger. 2. Machen Sie den Prozess Smss.exe ausfindig: lkd> !process 0 0 smss.exe PROCESS 8e54bc40 SessionId: none Cid: 0130 Peb: 02bab000 ParentCid: 0004 DirBase: bffe0020 ObjectTable: 8a767640 HandleCount: <Data Not Accessible> Image: smss.exe PROCESS 9985bc40 SessionId: 1 Cid: 01d4 Peb: 02f9c000 ParentCid: 0130 DirBase: bffe0080 ObjectTable: 00000000 HandleCount: 0. Image: smss.exe PROCESS a122dc40 SessionId: 2 Cid: 02a8 Peb: 02fcd000 ParentCid: 0130 DirBase: bffe0320 ObjectTable: 00000000 HandleCount: 0. Image: smss.exe → Seitenfehler Kapitel 5 445
3. Suchen Sie die erste Instanz (mit der Sitzung-ID none), also die Master-Instanz von Smss. exe (siehe Kapitel 2). 4. Schalten Sie den Debugger auf den Kontext dieses Prozesses um: lkd> .process /r /p 8e54bc40 Implicit process is now 8e54bc40 Loading User Symbols .. 5. Lassen Sie sich die vier zuvor beschriebenen Variablen anzeigen. (Sie sind jeweils 64 Bits groß.) lkd> dq smss!SmpDesiredPfSizeBasedOnRAM L1 00974cd0 00000000'4fff1a00 lkd> dq smss!SmpDesiredPfSizeBasedOnHistory L1 00974cd8 00000000'05a24700 lkd> dq smss!SmpDesiredPfSizeForCrashDump L1 00974cc8 00000000'1ffecd55 lkd> dq smss!SmpDesiredPfSizeForApps L1 00974ce0 00000000'00000000 6. Da der Computer nur ein einziges Volume hat (C:\), wird auch nur eine einzige Auslagerungsdatei erstellt. Wenn keine besondere Konfiguration vorliegt, wird sie vom System verwaltet. Sie können sich die tatsächliche Größe von C:\PageFile.sys auf der Festplatte ansehen oder den Debuggerbefehl !vm verwenden: lkd> !vm 1 Page File: \??\C:\pagefile.sys Current: 524288 Kb Free Space: Minimum: 524288 Kb Maximum: 524280 Kb 8324476 Kb Page File: \??\C:\swapfile.sys Current: 262144 Kb Free Space: Minimum: 262144 Kb Maximum: 262136 Kb 4717900 Kb No Name for Paging File Current: 11469744 Kb Free Space: 11443108 Kb Minimum: 11469744 Kb Maximum: 11469744 Kb ... Die Mindestgröße von C:\PageFile.sys beträgt 524.288 KB. (Was es mit den anderen Einträgen zur Auslagerungsdatei zu tun hat, schauen wir uns im nächsten Abschnitt an.) Wie Sie zuvor gesehen haben, ist von den Variablen SmpDesiredPfSizeForCrashDump die größte, weshalb sie der bestimmende Faktor ist (0x1FFECD55 = 524.211 KB). Dieser Wert liegt sehr nah an dem angezeigten Wert. (Die Größen von Auslagerungsdateien werden auf Mehrfache von 64 MB gerundet.) 446 Kapitel 5 Speicherverwaltung
Um eine neue Auslagerungsdatei hinzuzufügen, verwendet die Systemsteuerung den internen Systemdienst NtCreatePagingFile, der in Ntdll.dll definiert ist. Auslagerungsdateien werden immer nicht komprimiert erstellt, auch wenn das Verzeichnis, in dem sie sich befinden, komprimiert ist. Bis auf einige Ausnahmen, die im nächsten Abschnitt beschrieben werden, tragen sie den Namen PageFile.sys. Sie werden im Stamm der Partition mit dem Dateiattribut Versteckt erstellt, damit sie nicht unmittelbar sichtbar sind. Um zu verhindern, dass neue Auslagerungsdateien gelöscht werden, wird dem Systemprozess ein Duplikat des Handles gegeben, sodass auch dann, wenn der Erstellerprozess sein Handle schließt, immer ein offenes Handle vorhanden ist. Die UWP-Auslagerungsdatei Wenn eine UWP-App in den Hintergrund übergeht – etwa wenn sie minimiert wird –, werden die Threads in ihrem Prozess angehalten, sodass der Prozess keine CPU-Ressourcen verbraucht. Bei hoher Speicherbelastung kann der Arbeitssatz dieses Prozesses, also der von ihm verwendete private physische Arbeitsspeicher, auf die Festplatte ausgelagert werden, sodass andere Prozesse den Speicher nutzen können. In Windows 8 wurde eine besondere Auslagerungsdatei hinzugefügt, die ausschließlich für UWP-Anwendungen verwendet wird. Im Grunde genommen unterscheidet sie sich jedoch nicht von der regulären Auslagerungsdatei. (In der deutschen Microsoft-Terminologie wird sie auch schlicht als »Auslagerungsdatei« bezeichnet, obwohl im englischen Windows zwischen der regulären »page file« und dieser »swap file« unterschieden wird.) Auf Client-SKUs wird sie nur dann erstellt, wenn es bereits mindestens eine reguläre Auslagerungsdatei gibt (was normalerweise der Fall ist). Sie befindet sich als SwapFile.sys in der Systemstammpartition, zum Beispiel C:\SwapFile.sys. Nachdem die regulären Auslagerungsdateien erstellt worden sind, wird der Registrierungsschlüssel HKLM\System\CurrentControlSet\Control\SessionManager\Memory Management untersucht. Ist dort ein DWORD-Wert namens SwapFileControl vorhanden und auf 0 gesetzt, so wird keine UWP-Auslagerungsdatei erstellt. Existiert dort dagegen ein Wert namens SwapFile, so wird er als String mit demselben Format wie für eine reguläre Auslagerungsdatei gelesen, also mit einem Dateinamen, einer Anfangs- und einer Höchstgröße. Der Unterschied zu dem String für eine reguläre Auslagerungsdatei besteht darin, dass bei einem Wert von 0 für die Größen keine UWP-Auslagerungsdatei erstellt wird. Da diese beiden Registrierungswerte standardmäßig nicht vorhanden sind, wird in der Systemstammpartition SwapFile.sys mit einer Mindestgröße von 16 MB auf schnellen (und kleinen) Festplatten (z. B. SSD) bzw. 256 MB auf langsamen Festplatten (oder großen SSD-Festplatten) erstellt. Als Maximalgröße der UWP-Austauschdatei wird der kleinere Wert von entweder 1,5 * RAM oder 10 % der Systemstammpartition festgelegt. Mehr über UWP-Apps erfahren Sie in Kapitel 7 dieses Buches sowie in Kapitel 8 und 9 von Band 2. Die UWP-Auslagerungsdatei wird bei der Höchstzahl der möglichen Auslagerungsdateien nicht mitgezählt. Seitenfehler Kapitel 5 447
Die virtuelle Auslagerungsdatei In der Ausgabe des Debuggerbefehls !vm haben Sie noch eine weitere Auslagerungsdatei gesehen, die als No Name for Paging File gekennzeichnet war. Dies ist eine virtuelle Auslagerungsdatei. Wie der Name schon andeutet, ist mit ihr keine echte Datei verbunden. Stattdessen dient sie indirekt als Hintergrundspeicher für die Speicherkomprimierung (die wir weiter hinten in diesem Kapitel im Abschnitt »Speicherkomprimierung« eingehender betrachten werden). Sie ist sehr groß, doch ihre Größe wird willkürlich festgelegt, damit ihr der freie Speicher nicht ausgeht. Ungültige PTEs für Seiten, die komprimiert wurden, zeigen auf diese virtuelle Auslagerungsdatei. Dadurch kann der Komprimierungsspeicher komprimierte Daten bei Bedarf abrufen, indem er die Bits in diesen ungültigen PTEs untersucht und sich von ihnen zum richtigen Index in der richtigen Region des richtigen Speichers führen lässt. Experiment: Informationen über die UWP- und die virtuelle Auslagerungsdatei einsehen Der Debuggerbefehl !vm zeigt Informationen über alle Auslagerungsdateien an, einschließlich der UWP- und der virtuellen Auslagerungsdatei. lkd> !vm 1 Page File: \??\C:\pagefile.sys Current: 524288 Kb Free Space: 524280 Kb Minimum: 524288 Kb Maximum: 8324476 Kb Page File: \??\C:\swapfile.sys Current: 262144 Kb Free Space: 262136 Kb Minimum: 262144 Kb Maximum: 4717900 Kb No Name for Paging File Current: 11469744 Kb Free Space: 11443108 Kb Minimum: 11469744 Kb Maximum: 11469744 Kb Auf diesem System hat die UWP-Auslagerungsdatei eine Mindestgröße von 256 MB, da es sich bei ihm um eine VM mit Windows 10 handelt und die VHD als langsame Festplatte angesehen wird. Der RAM auf dem System beträgt 3 GB und die Festplattenpartition umfasst 64 GB. Als Maximalgröße wird daher der kleinere Wert von 4,5 GB (1,5 * RAM) und 6,4 GB (10 % der Festplattengröße) genommen, also 4,5 GB. Gesamter zugesicherter Speicher und systemweiter Zusicherungsgrenzwert Inzwischen verfügen Sie über ausreichend Kenntnisse, um sich eingehender mit dem gesamten zugesicherten Speicher und dem systemweiten Zusicherungsgrenzwert beschäftigen zu können. Wenn virtueller Adressraum erstellt wird – z. B. durch VirtualAlloc für zugesicherten Speicher oder durch MapViewOfFile –, muss das System dafür sorgen, dass entweder im RAM 448 Kapitel 5 Speicherverwaltung
oder im Hintergrundspeicher genügend Platz dafür vorhanden ist. Für zugeordneten Speicher (außer den Abschnitten, die Auslagerungsdateien zugeordnet sind) bildet die Datei, die mit dem im MapViewOfFile-Aufruf genannten Zuordnungsobjekt verbunden ist, den erforderlichen Hintergrundspeicher. Bei allen anderen virtuellen Zuweisungen wird auf die vom System verwalteten gemeinsam genutzten Speicherressourcen zurückgegriffen, also den RAM und die Auslagerungsdateien. Der gesamte zugesicherte Speicher und der systemweite Zusicherungsgrenzwert dienen dazu, die Übersicht über alle diese Ressourcen zu wahren, damit es nie zu einer Überzusicherung kommen kann, das heißt, damit niemals mehr virtueller Adressraum definiert wird, als Platz zur Speicherung seiner Inhalte im RAM und im Hintergrundspeicher (auf der Festplatte) zur Verfügung steht. In diesem Abschnitt werden häufig die Auslagerungsdateien erwähnt. Es ist durchaus möglich (im Allgemeinen aber nicht empfehlenswert), Windows ohne jegliche Auslagerungsdateien zu betreiben. Wenn der RAM erschöpft ist, gibt es dann keinen Ausweichplatz mehr, sodass Speicherzuweisungen fehlschlagen. Das führt zu einem Bluescreen. Bei jedem Hinweis auf eine Auslagerungsdatei können Sie im Stillen ergänzen: »... falls mindestens eine Auslagerungsdatei vorhanden ist.« Der systemweite Zusicherungsgrenzwert gibt an, wie viel zugesicherter virtueller Adressraum insgesamt erstellt werden kann – neben den virtuellen Zuweisungen, die über ihren eigenen Hintergrundspeicher verfügen, also neben den Abschnitten, die Dateien zugeordnet sind. Der numerische Wert ist einfach die Menge des RAM, die für Windows zur Verfügung steht, zuzüglich der aktuellen Größen aller vorhandenen Auslagerungsdateien. Wird eine Auslagerungsdatei erweitert oder eine neue erstellt, so wird der Zuweisungsgrenzwert entsprechend erhöht. Ist keine Auslagerungsdatei vorhanden, so entspricht der systemweite Zusicherungsgrenzwert einfach der Gesamtmenge des für Windows verfügbaren RAM. Der gesamte zugesicherte Speicher ist die systemweite Summe sämtlicher zugesicherten Speicherzuweisungen, die im RAM oder in einer Auslagerungsdatei gehalten werden müssen. Dazu tragen neben den privaten zugesicherten Adressräumen der Prozesse auch noch viele andere, weniger offensichtliche Dinge bei. Windows führt auch die Prozessquote für die Auslagerungsdatei. Viele der Zuweisungen tragen nicht nur zum gesamten zugesicherten Speicher, sondern auch zu dieser Quote bei. Sie steht jeweils für den privaten Beitrag des Prozesses zum gesamten zugesicherten Speicher. Allerdings gibt sie nicht die aktuelle Nutzung der Auslagerungsdatei an, sondern die potenzielle oder maximal mögliche Nutzung, wenn alle diese Zuweisungen dort gespeichert würden. Die folgenden Arten von Speicherzuweisungen tragen zum gesamten zugesicherten Speicher und in vielen Fällen auch zur Prozessquote für die Auslagerungsdatei bei. Einige von ihnen sehen wir uns in späteren Abschnitten dieses Kapitels noch genauer an. QQ Privater zugesicherter Speicher Hierbei handelt es sich um den Speicher, der mit V­ irtualAlloc und der Option MEM_COMMIT zugewiesen wird. Dies sind die üblichsten Zuweisungen, die zum gesamten zugesicherten Speicher und auch zur Prozessquote für die Auslagerungsdatei beitragen. Seitenfehler Kapitel 5 449
QQ Auslagerungsgepufferter, zugeordneter Speicher Dieser Speicher wird mit einem Aufruf von MapViewOfFile zugesichert, der auf ein nicht mit einer Datei verknüpftes Abschnittsobjekt verweist. Stattdessen verwendet das System einen Teil der Auslagerungsdatei als Hintergrundspeicher. Diese Zuweisungen werden der Prozessquote für die Auslagerungsdatei nicht angerechnet. QQ Copy-on-Write-Regionen des zugeordneten Speichers (einschließlich solcher, die mit regulären zugeordneten Dateien verknüpft sind) Die zugeordnete Datei bildet den Hintergrundspeicher für ihren eigenen nicht modifizierten Inhalt. Wird eine Seite in einer Copy-on-Write-Region geändert, kann sie die ursprünglich zugeordnete Datei nicht mehr als Hintergrundspeicher nutzen, sondern muss im RAM festgehalten oder in eine Auslagerungsdatei gestellt werden. Diese Zuweisungen werden der Prozessquote für die Auslagerungsdatei nicht angerechnet. QQ Zuweisungen aus dem nicht ausgelagerten und dem ausgelagerten Pool sowie andere Zuweisungen im Systemraum, die nicht ausdrücklich über zugehörige Dateien als Hintergrundspeicher verfügen Selbst die zurzeit freien Regionen der Systemspeicherpools tragen zum gesamten zugesicherten Speicher bei. Die nicht auslagerungsfähigen Regionen werden beim gesamten zugesicherten Speicher mitgezählt, auch wenn sie niemals in eine Auslagerungsdatei geschrieben werden, da sie den für private auslagerungsfähige Daten verfügbaren RAM dauerhaft verringern. Diese Zuweisungen werden der Prozessquote für die Auslagerungsdatei nicht angerechnet. QQ Kernelstacks Threadstacks bei der Ausführung im Kernelmodus. QQ Seitentabellen Die meisten davon können selbst ausgelagert werden und haben keine zugeordnete Dateien als Hintergrundspeicher. Wenn sie aber nicht auslagerungsfähig sind, nehmen sie RAM ein. Daher trägt der für sie benötigte Platz zum gesamten zugesicherten Speicher bei. QQ Platz für noch nicht zugewiesene Seitentabellen Wenn große Bereiche des virtuellen Adressraums zwar definiert wurden, aber noch kein Zugriff auf sie erfolgt ist (z. B. bei privatem zugesichertem virtuellem Adressraum), muss das System noch keine Seitentabellen dafür erstellen. Der Platz für diese noch nicht vorhandenen Seitentabellen wird jedoch dem gesamten zugesicherten Speicher zugeschlagen, damit die Seitentabellen bei Bedarf angelegt werden können. QQ Zuweisungen von physischem Speicher über AWE-APIs Wie bereits erwähnt, verbrauchen solche Zuweisungen direkt physischen Arbeitsspeicher. Bei vielen dieser Punkte steht der Beitrag zum gesamten zugesicherten Speicher nur für eine mögliche und nicht für die tatsächliche Nutzung des Speichers. Beispielsweise nimmt eine Seite im privaten zugesicherten Speicher erst dann tatsächlich eine physische Seite im RAM oder den entsprechenden Platz in der Auslagerungsdatei ein, wenn mindestens einmal auf sie verwiesen wurde. Bis dahin handelt es sich um eine Nullforderungsseite (was weiter hinten erklärt wird). Beim gesamten zugesicherten Speicher wird diese Seite jedoch mitgezählt, wenn der virtuelle Speicher für sie angelegt wird. Dadurch ist garantiert, dass der physische Speicher verfügbar ist, wenn später auf die Seite verwiesen wird. 450 Kapitel 5 Speicherverwaltung
Bei Copy-on-Write-Regionen für zugeordnete Dateien gilt Ähnliches. Bis der Prozess in die Region schreibt, verwenden alle Seiten darin die zugeordnete Datei als Hintergrundspeicher. Allerdings kann der Prozess jederzeit auf irgendeine dieser Seiten schreiben, und wenn das geschieht, werden diese Seiten anschließend als private Seiten des Prozesses betrachtet, sodass dann die Auslagerungsdatei ihren Hintergrundspeicher bildet. Da die Region beim Erstellen dem gesamten zugesicherten Speicher zugeschlagen wird, ist garantiert, dass später bei einem Schreibzugriff privater Speicher für sie zur Verfügung steht. Ein besonderer Fall liegt vor, wenn privater Speicher reserviert und später zugesichert wird. Wurde die reservierte Region mit VirtualAlloc erstellt, wird sie nicht auf den gesamten zugesicherten Speicher angerechnet. In Windows 8 und Windows Server 2012 sowie früheren Versionen wurden jedoch alle Seiten für neue Seitentabellen angerechnet, die zur Beschreibung dieser Region erforderlich sind, auch wenn sie noch gar nicht existieren oder möglicherweise nie benötigt werden. Ab Windows 8.1 und Windows Server 2012 R2 werden die Seitentabellenhierarchien für reservierte Regionen jedoch nicht mehr sofort angerechnet. Dadurch können riesige reservierte Regionen ohne Seitentabellen zugewiesen werden, ohne den den Speicher zu erschöpfen, was insbesondere für einige Sicherheitseinrichtungen wie Control Flow Guard (siehe Kapitel 7) wichtig ist. Wird die Region oder ein Teil davon später zugesichert, so wird die Größe dieser Region und ihrer Seitentabellen auf den gesamten zugesicherten Speicher und die Prozessquote für die Auslagerungsdatei angerechnet. Anders ausgedrückt: Wenn das System eine VirtualAlloc-Zusicherung oder einen MapViewOfFile-Aufruf erfolgreich abschließt, garantiert es, dass der erforderliche Speicher bei Bedarf verfügbar sein wird, auch wenn er zurzeit nicht gebraucht wird. Dadurch können spätere Speicherverweise auf die zugewiesene Region niemals aufgrund von Speichermangel fehlschlagen (allerdings immer noch aus anderen Gründen, z. B. den Seitenschutzeinstellungen, der Aufhebung der Zuweisung für die Region usw.). Der Mechanismus für die Anrechnung auf den gesamten zugesicherten Speicher ermöglicht es dem System, seine Zusicherungen einzuhalten. In der Leistungsüberwachung wird der gesamte zugesicherte Speicher mit dem Indikator Arbeitsspeicher: Zugesicherte Bytes angezeigt. Sie finden ihn auch als die erste der beiden Zahlen unter der Beschriftung Commit ausgeführt auf der Registerkarte Leistung des Task-­Managers (wobei die zweite Zahl den Zusicherungsgrenzwert angibt) und als Commit Charge – Current auf der Registerkarte Memory des Dialogfelds System Information von Process Explorer. Die Prozessquote für die Auslagerungsdatei wird in den Leistungsindikatoren Prozess: Auslagerungsdatei (Bytes) und Prozess: Private Bytes angezeigt. Keiner dieser beiden Begriffe beschreibt die Bedeutung dieses Indikators jedoch korrekt. Wenn der gesamte zugesicherte Speicher den Zuweisungsgrenzwert erreicht, versucht der Speicher-Manager, den Grenzwert zu erhöhen, indem er eine oder mehrere Auslagerungsdateien vergrößert. Falls das nicht möglich ist, schlagen anschließende Versuche zur Zuweisung von virtuellem Arbeitsspeicher fehl, der auf den gesamten zugesicherten Arbeitsspeicher angerechnet wird, bis bereits vorhandener zugesicherter Speicher freigegeben wird. Mit den in Tabelle 5–13 aufgeführten Leistungsindikatoren können Sie die Nutzung von privatem zugesichertem Speicher für das gesamte System, für einzelne Prozesse und für einzelne Auslagerungsdateien untersuchen. Seitenfehler Kapitel 5 451
Leistungsindikator Beschreibung Arbeitsspeicher: Zugesicherte Bytes Dies ist die Größe (in Bytes) des virtuellen (nicht reservierten) Speichers, der zugesichert wurde. Sie gibt nicht unbedingt die Nutzung der Auslagerungsdatei wieder, da darin auch private zugesicherte Seiten im physischen Speicher enthalten sind, die niemals ausgelagert wurden. Tatsächlich steht sie für die angerechnete Menge des zugesicherten Speichers, für den Platz in der Auslagerungsdatei oder im RAM zur Verfügung stehen muss. Arbeitsspeicher: Zusagegrenzwert Dies ist die Größe (in Bytes) des virtuellen Speichers, der zugesichert werden kann, ohne die Auslagerungsdateien erweitern zu müssen. Ist eine Vergrößerung der Auslagerungsdateien möglich, so ist dies ein weicher Grenzwert. Prozess: Quoten für Auslagerungsdatei Dies ist der Beitrag des Prozesses zu Arbeitsspeicher: Zugesicherte Bytes. Prozess: Private Bytes Identisch mit Prozess: Quoten für Auslagerungsdatei Prozess: Arbeitsseiten – privat Dies ist der Anteil von Prozess: Quoten für Auslagerungsdatei, der sich zurzeit im RAM befindet und auf den ohne Seitenfehler verwiesen werden kann. Dies ist auch ein Anteil von Prozess: Arbeitsseiten. Prozess: Arbeitsseiten Dies ist der Anteil von Prozess: Virtuelle Bytes, der sich zurzeit im RAM befindet und auf den ohne Seitenfehler verwiesen werden kann. Prozess: Virtuelle Größe Dies ist der Gesamtumfang der virtuellen Speicherzuweisungen des Prozesses einschließlich zugeordneter, privater zugesicherter und privater reservierter Regio­ nen. Auslagerungsdatei: Belegung (%) Dies ist der prozentuale Anteil des Platzes in der Auslagerungsdatei, der zurzeit verwendet wird. Auslagerungsdatei: Maximale Belegung (%) Dies ist der höchste bisher aufgetretene Wert von Auslagerungsdatei: Belegung (%). Tabelle 5–13 Leistungsindikatoren für zugesicherten Speicher und Auslagerungsdateien Der Zusammenhang zwischen dem gesamten zugesicherten Speicher und der Größe der Auslagerungsdatei Anhand der Zähler in Tabelle 5–13 können Sie die Größe der Auslagerungsdatei auch selbst bestimmen. Da die Standardfestlegung auf der Größe des RAM beruht, ist er für die meisten Rechner geeignet, doch je nach Arbeitslast kann die resultierende Auslagerungsdatei dabei auch unnötig groß oder zu klein sein. Um zu ermitteln, wie viel Platz in der Auslagerungsdatei wirklich für die Anwendungen gebraucht wird, die seit dem Systemstart ausgeführt wurden, schauen Sie sich den Spitzenwert des gesamten zugesicherten Speichers (Commit Charge – Peak) auf der Registerkarte Memory des Dialogfelds System Information von Process Explorer an. Diese Zahl gibt die maximale Größe der Auslagerungsdatei an, die seit dem Systemstart nötig gewesen wäre, wenn das System 452 Kapitel 5 Speicherverwaltung
den Großteil des privaten zugesicherten virtuellen Speichers auslagern müsste (was selten der Fall ist). Wenn die Auslagerungsdatei auf Ihrem System zu groß ist, verwendet das System sie auch nicht mehr oder weniger als sonst. Anders ausgedrückt, die Vergrößerung der Auslagerungsdatei ändert die Systemleistung nicht, sondern bedeutet lediglich, dass das System über mehr zugesicherten virtuellen Speicher verfügt. Ist die Auslagerungsdatei zu klein für die ausgeführten Anwendungen, kann es vorkommen, dass eine Fehlermeldung über mangelnden virtuellen Speicher angezeigt wird. Prüfen Sie in einem solchen Fall, ob einer der Prozesse ein Speicherleck aufweist, indem Sie sich die Anzahl seiner privaten Bytes ansehen. Scheint es kein Leck zu geben, so überprüfen Sie die Größe des ausgelagerten Systempools. Wenn ein Gerätetreiber ein Speicherleck in diesem Pool aufweist, kann ebenfalls die Fehlermeldung auftreten. Wie Sie weiter vorgehen, erfahren Sie im Experiment »Fehlerbehebung bei einem Poolleck« im Abschnitt »Kernelmodusheaps (Systemspeicherpools)« weiter vorn in diesem Kapitel. Experiment: Die Nutzung der Auslagerungsdatei im TaskManager untersuchen Sie können sich die Nutzung des zugesicherten Speichers auch im Task-Manager ansehen. Auf der Registerkarte Leistung finden Sie dort die folgenden Indikatoren im Zusammenhang mit Auslagerungsdateien: → Seitenfehler Kapitel 5 453
Unter Commit ausgeführt finden Sie zwei Zahlen für den gesamten zugesicherten Speicher. Die erste gibt nicht die tatsächliche, sondern die mögliche Nutzung der Auslagerungsdatei an, also wie viel Platz in der Auslagerungsdatei gebraucht würde, wenn der gesamte private zugesicherte virtuelle Speicher im System auf einmal ausgelagert werden müsste. Die zweite Zahl ist der Zusicherungsgrenzwert, also die Höchstmenge an virtuellem Speicher, die das System zur Verfügung stellen kann. Dies schließt sowohl den auslagerungsgepufferten virtuellen Speicher ein als auch denjenigen, der den physischen Speicher als Hintergrundspeicher nutzt. Dieser Grenzwert ergibt sich aus der Größe des RAM plus der aktuellen Größe der Auslagerungsdateien, also ohne Berücksichtigung möglicher Erweiterungen der Auslagerungsdatei. Im Dialogfeld System Information von Process Explorer finden Sie noch eine weitere Information über die Nutzung des zugesicherten Speichers, nämlich das Verhältnis des Spitzenwerts und der aktuellen Nutzung zum Grenzwert: Stacks Wenn ein Thread ausgeführt wird, muss er Zugriff auf einen temporären Speicherort für Funktionsparameter, lokale Variablen und die Rückkehradresse haben. Dieser Teil des Speichers wird als Stack bezeichnet. In Windows stellt der Speicher-Manager für jeden Thread zwei Stacks bereit, nämlich den Benutzer- und den Kernelstack. Darüber hinaus hat jeder Prozessor noch seinen DPC-Stack. In Kapitel 2 wurde kurz beschrieben, wie Systemaufrufe ein Umschalten des Threads von seinem Benutzer- zum Kernelstack verursachen. Jetzt wollen wir uns einige zusätzliche Dienste ansehen, die der Speicher-Manager für die effiziente Nutzung des Platzes für den Benutzerstack zur Verfügung stellt. 454 Kapitel 5 Speicherverwaltung
Benutzerstacks Beim Erstellen eines Threads reserviert der Speicher-Manager automatisch eine zuvor festgelegte Menge an virtuellem Speicher, standardmäßig 1 MB. Diesen Betrag können Sie mit den Funktionen CreateThread und CreateRemoteThread(Ex) festlegen, aber auch beim Kompilieren der Anwendung mit dem Schalter /STACK:reserve des C/C++-Compilers von Microsoft in den Abbildheader aufnehmen. Es wird zwar 1 MB reserviert, doch nur die erste Seite des Stacks sowie eine Wächterseite werden zugesichert (es sei denn, im PE-Header des Abbilds ist etwas anderes festgelegt). Wenn der Stack so weit wächst, dass er die Wächterseite erreicht, wird eine Ausnahme ausgelöst, durch die wiederum versucht wird, eine weitere Wächterseite zuzuweisen. Dank dieses Mechanismus verbraucht der Stack nicht sofort alle 1 MB des zugesicherten Speichers, sondern wächst bei Bedarf. Allerdings kann er nicht wieder schrumpfen. Experiment: Die maximale Anzahl an Threads erstellen Während für jeden 32-Bit-Prozess nur 2 GB Benutzeradressraum zur Verfügung stehen, wird für die Stacks der einzelnen Threads jeweils eine relativ große Menge an Speicher reserviert. Daraus lässt sich leicht die höchstmögliche Anzahl an Threads in einem Prozess errechnen: Es sind knapp 2048 für fast 2 GB (also ohne die Verwendung der BCD-Option increaseuserva, durch die das Abbild einen größeren Adressraum nutzen kann). Wenn wir dafür sorgen, dass jeder neue Thread nur die kleinstmögliche Stackreservierungsgröße von 64 KB nutzt, können wir diesen Grenzwert jedoch auf ca. 30.000 Threads erweitern. Das können Sie mit TestLimit von Sysinternals selbst ausprobieren. Betrachten Sie die folgende Beispielausgabe: C:\Tools\Sysinternals>Testlimit.exe -t -n 64 Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - www.sysinternals.com Process ID: 17260 Creating threads with 64 KB stacks... Created 29900 threads. Lasterror: 8 Auf einer 64-Bit-Windows-Installation, auf der 128 TB an Benutzeradressraum zur Verfügung stehen, sollte man eigentlich Hunderttausende von Threads erwarten (sofern genügend Speicher verfügbar ist). Tatsächlich aber legt TestLimit weniger Threads an als auf einem 32-Bit-Rechner. Das liegt daran, dass TestLimit.exe eine 32-Bit-Anwendung ist und daher in der Wow64-Umgebung läuft (siehe Kapitel 8 in Band 2). Dabei hat jeder Thread aber neben seinem 32-Bit-Wow64-Stack auch noch seinen 64-Bit-Stack, sodass er doppelt so viel Speicher dafür verbraucht, während ihm gleichzeitig nur 2 GB an Adressraum zur Verfügung stehen. Um den Grenzwert für die Threadanzahl auf 64-Bit-Windows korrekt zu prüfen, müssen Sie daher TestLimit64.exe verwenden. → Stacks Kapitel 5 455
Um TestLimit abzubrechen, müssen Sie Process Explorer oder den Task-Manager verwenden. Mit (Strg) + (C) geht es nicht, da für diese Operation selbst ein neuer Thread angelegt werden muss, was nicht mehr möglich ist, wenn der Speicher erschöpft ist. Kernelstacks Während die typische Größe von Benutzerstacks 1 MB beträgt, wird für den Kernelstack erheblich weniger Speicher zur Verfügung gestellt, nämlich 12 KB auf 32- und 16 KB auf 64-Bit-Systemen. Hinzu kommt noch die Wächterseite, sodass sich insgesamt 16 bzw. 20 KB ergeben. Im Kernel ausgeführter Code soll weniger rekursiv sein als Benutzercode, Variablen effizienter verwenden und den Umfang der Stackpuffer gering halten. Da sich die Kernelstacks im System­ adressraum befinden (der von allen Prozessen gemeinsam genutzt wird), hat ihre Speichernutzung eine stärkere Auswirkung auf das System. Kernelcode ist zwar gewöhnlich nicht rekursiv, doch Interaktionen zwischen Grafiksystemaufrufen, die von Win32k.sys gehandhabt werden, und nachfolgenden Callbacks in den Benutzermodus können rekursive Wiedereintrittsvorgänge in den Kernel auf demselben Kernelstack hervorrufen. Daher stellt Windows einen Mechanismus bereit, um den Kernelstack von seiner ursprünglichen Größe zu erweitern und wieder zu schrumpfen. Bei jedem weiteren Grafikaufruf des Threads werden weitere 16 KB Speicher für den Kernelstack zugewiesen. (Dies kann irgendwo im Systemadressraum geschehen. Der Speicher-Manager bietet die Möglichkeit, bei der Annäherung an die Wächterseite zu einem anderen Stack zu springen.) Wenn ein Aufruf die Steuerung an den Aufrufer zurückgibt (»abwickelt«), gibt der Speicher-Manager den dafür zugewiesenen zusätzlichen Kernelstack wieder frei (siehe Abb. 5–28). Dieser Mechanismus ermöglicht eine zuverlässige Ausführung rekursiver Systemaufrufe bei gleichzeitiger effizienter Nutzung des Systemadressraums. Er wird von Treiberentwicklern bei rekursiven Callouts durch die KeExpandKernelStackAndCallout(Ex)-APIs bei Bedarf zur Verfügung gestellt. Kernelmodusstack mit 16/20 KB Zusätzlicher Stack mit 16/20 KB Zusätzlicher Stack mit 16/20 KB Abbildung 5–28 Springen zwischen den Kernelstacks 456 Kapitel 5 Speicherverwaltung Abwickeln beim Abschluss der verschachtelten Callbacks
Experiment: Die Nutzung des Kernelstacks untersuchen Mit dem Sysinternals-Tools RamMap können Sie sich ansehen, wie viel physischen Speicher die Kernelstacks zurzeit belegen. Der folgende Screenshot zeigt die Registerkarte Use Counts dieses Programms: Um sich die Nutzung der Kernelstacks anzusehen, gehen Sie wie folgt vor: 1. Wiederholen Sie das vorherige TestLimit-Experiment, beenden Sie TestLimit aber nicht. 2. Wechseln Sie zu RamMap. 3. Öffnen Sie das Menü File und klicken Sie auf Refresh (oder drücken Sie (F5)). Jetzt wird eine viel höhere Größe für die Kernelstacks angezeigt: Wenn Sie TestLimit mehrere Male ausführen, ohne die vorherigen Instanzen zu schließen, können Sie auf einem 32-Bit-System den physischen Speicher sehr leicht erschöpfen. Daraus ergibt sich eine der wichtigsten Beschränkungen für die systemweite Threadanzahl auf 32-Bit-Systemen. Der DPC-Stack Windows hält für jeden Prozessor einen DPC-Stack bereit, den das System nutzen kann, wenn DPCs ausgeführt werden. Dadurch wird der DPC-Code vom Kernelstack des laufenden Threads getrennt, der schließlich gar nichts mit der DPC-Operation zu tun hat, da DPCs in einem willkürlichen Threadkontext ausgeführt werden (siehe Kapitel 6). Der DPC-Stack ist auch als Anfangsstack für die Handhabung der Anweisung sysenter (x86), svc (ARM) bzw. syscall (x64) während eines Systemaufrufs eingerichtet. Die CPU ist dafür zuständig, den Stack zu wechseln, wenn diese Anweisungen ausgeführt werden. Dazu zieht sie ein vom Modell abhängiges Register heran (ein MSR auf x86/x64). Eine Umprogrammierung des MSR bei jedem Kontextwechsel wäre jedoch eine ziemlich kostspielige Operation, weshalb Windows im MSR nur einen Zeiger auf den DPC-Stack des Prozessors einrichtet. VADs Der Speicher-Manager verwendet einen bedarfsgesteuerten Auslagerungsmechanismus, um zu erfahren, wann er Seiten in den Arbeitsspeicher laden soll. Dabei wartet er, bis ein Thread auf eine Adresse verweist und einen Seitenfehler hervorruft, bevor er die Seite von der Festplatte zurückholt. Ebenso wie das Kopieren beim Schreiben ist auch dieser Mechanismus eine VADs Kapitel 5 457
Form von verzögerter Auswertung – die Erledigung einer Aufgabe wird bis zu dem Zeitpunkt verschoben, an dem sie tatsächlich erforderlich ist. Der Speicher-Manager setzt die verzögerte Auswertung nicht nur ein, um Seiten in den Arbeitsspeicher zurückzuholen, sondern auch, um die zur Beschreibung neuer Seiten erforderlichen Seitentabellen anzulegen. Wenn ein Thread mit VirtualAlloc eine umfangreiche Region im virtuellen Speicher zusichert, könnte der Speicher-Manager die für den Zugriff auf diese Region erforderlichen Seitentabellen sofort erstellen. Was aber, wenn auf einige Teile dieser Region niemals zugegriffen wird? Seitentabellen für die gesamte Region zu erstellen wäre Verschwendung. Stattdessen wartet der Speicher-Manager damit, bis ein Thread einen Seitenfehler hervorruft. Erst dann erstellt er eine Seitentabelle für die betreffende Seite. Diese Methode führt zu einer erheblichen Leistungssteigerung von Prozessen, die sehr viel Speicher reservieren oder zusichern, aber nur spärlich darauf zugreifen. Der virtuelle Adressraum, der von diesen noch nicht vorhandenen Seitentabellen eingenommen werden würde, wird dem gesamten zugesicherten Speicher und der Prozessquote für die Auslagerungsdatei zugewiesen. Dadurch ist garantiert, dass der Platz für sie vorhanden ist, wenn sie erstellt werden. Dank der verzögerten Auswertung bildet auch die Zuweisung umfangreicher Speicherblöcke eine schnelle Operation. Wenn ein Thread Speicher zuweist, muss der Speicher-Manager mit einem Adressbereich antworten, den der Thread nutzen kann. Dazu unterhält der Speicher-Manager einen weiteren Satz von Datenstrukturen, in denen er sich merkt, welche virtuellen Adressen im Adressraum des Prozesses reserviert worden sind und welche nicht. Diese Datenstrukturen werden als Virtual Address Descriptors (VADs) bezeichnet und aus dem nicht ausgelagerten Pool zugewiesen. Prozess-VADs Für jeden Prozess unterhält der Speicher-Manager einen Satz von VADs, die den Status des Adressraums für diesen Prozess beschreiben. Die VADs sind in einem selbst ausgleichenden AVL-Baum angeordnet (benannt nach seinen Erfindern Adelson-Velsky und Landis). Die Höhen der beiden Kindknoten eines beliebigen Knotens dürfen darin höchstens um 1 auseinanderliegen, was Einfügen, Nachschlagen und Löschen sehr schnell macht. Dank der Verwendung eines AVL-Baums ist bei der Suche nach einem VAD für eine virtuelle Adresse gewöhnlich nur die Mindestanzahl von Vergleichen erforderlich. Für jeden praktisch zusammenhängenden Bereich nicht freier virtueller Adressen derselben Art (reserviert, zugesichert oder zugeordnet, gleicher Zugriffsschutz usw.) gibt es einen VAD. Ein Diagramm des VAD-Baums sehen Sie in Abb. 5–29. 458 Kapitel 5 Speicherverwaltung
Bereich: 20000000 bis 2000FFFF Schutz: Lesen/Schreiben Bereich: 000080000 bis 0000DFFF Schutz: Nur Lesen Bereich: 4A000000 bis 4C00FFFF Schutz: Kopieren beim Schreiben Bereich: 33000000 bis 3300FFFF Schutz: Nur Lesen Bereich: 52000000 bis 52017FFF Schutz: Lesen/Schreiben Abbildung 5–29 VADs Wenn ein Prozess Adressraum reserviert oder die Sicht eines Abschnitts zuordnet, erstellt der Speicher-Manager einen VAD, um darin jegliche Informationen festzuhalten, die bei der Zuweisungsanforderung übergeben wurden, z. B. den Bereich der reservierten Adresse, den Schutz für die Seiten in dem Bereich, die Angabe, ob der Bereich gemeinsam nutzbar oder privat ist, und die Angabe, ob ein Kindprozess den Inhalt des Bereichs erben kann. Wenn zum ersten Mal ein Thread auf eine Adresse zugreift, muss der Speicher-Manager einen PTE für die Seite erstellen, die diese Adresse enthält. Dazu sucht er den VAD, in dessen Adressbereich sich die Adresse befindet, und füllt den PTE mit den darin aufgezeichneten Informationen aus. Wird die Adresse von keinem VAD abgedeckt oder befindet sie sich in einem Bereich, der zwar reserviert, aber nicht zugesichert ist, weiß der Speicher-Manager, dass der Thread versucht, auf Speicher zuzugreifen, ohne ihn vorher zugewiesen zu haben, und löst daher eine Zugriffsverletzung aus. Experiment: VADs einsehen Mit dem Kerneldebuggerbefehl !vad können Sie sich die VADs eines Prozesses ansehen. Dazu müssen Sie zunächst mit !process die Adresse für den Stamm des VAD-Baums herausfinden. Diese Adresse übergeben Sie anschließend an !vad. Das folgende Beispiel zeigt diesen Vorgang für den VAD-Baum des Prozesses, der Explorer.exe ausführt: lkd> !process 0 1 explorer.exe PROCESS ffffc8069382e080 SessionId: 1 Cid: 43e0 Peb: 00bc5000 ParentCid: 0338 DirBase: 554ab7000 ObjectTable: ffffda8f62811d80 HandleCount: 823. Image: explorer.exe VadRoot ffffc806912337f0 Vads 505 Clone 0 Private 5088. Modified 2146. Locked 0. ... lkd> !vad ffffc8068ae1e470 VAD Level Start End Commit → VADs Kapitel 5 459
ffffc80689bc52b0 9 640 64f 0 Mapped READWRITE 0 Mapped READONLY 0 Mapped READONLY Pagefile section, shared commit 0x10 ffffc80689be6900 8 650 651 Pagefile section, shared commit 0x2 ffffc80689bc4290 9 660 675 Pagefile section, shared commit 0x16 ffffc8068ae1f320 7 680 6ff 32 Private READWRITE ffffc80689b290b0 9 700 701 2 Private READWRITE ffffc80688da04f0 8 710 711 2 Private READWRITE ffffc80682795760 6 720 723 0 Mapped READONLY 0 Mapped READONLY Pagefile section, shared commit 0x4 ffffc80688d85670 10 730 731 Pagefile section, shared commit 0x2 ffffc80689bdd9e0 9 740 741 2 Private READWRITE ffffc80688da57b0 8 750 755 0 Mapped READONLY \Windows\en-US\explorer.exe.mui ... Total VADs: 574, average level: 8, maximum depth: 10 Total private commit: 0x3420 pages (53376 KB) Total shared commit: 0x478 pages (4576 KB) Umlauf-VADs Videokartentreiber müssen gewöhnlich Daten von einer Grafikanwendung im Benutzermodus in verschiedene Stellen des Systemarbeitsspeichers kopieren, u. a. in den Speicher der Videokarte und des AGP-Ports, die beide andere Cacheattribute und Adressen aufweisen. Um diese verschiedenen Speichersichten schnell einem Prozess zuordnen zu können und dabei unterschiedliche Cacheattribute zuzulassen, verwendet der Speicher-Manager Umlauf-VADs. Damit kann ein Videotreiber Daten direkt mithilfe der GPU übertragen und Speicher nach Bedarf durch ein Umlaufverfahren aus den Seiten der Prozesssicht herausnehmen und einfügen. Abb. 5–30 zeigt an einem Beispiel, wie dieselbe virtuelle Adresse zwischen dem Video-RAM und dem virtuellen Speicher gewechselt werden kann. 460 Kapitel 5 Speicherverwaltung
Video-RAM Virtueller Adressraum Seitentabelle Benutzerdaten Virtuelle Benutzer­adresse Eintrag für virtuelle Benutzer­adresse Auslagerungs­ gepufferte Seite Benutzerdaten Abbildung 5–30 Umlauf-VADs NUMA Bei jedem neuen Release von Windows gibt es Verbesserungen am Speicher-Manager, um die Möglichkeiten von NUMA-Computern (Non-Uniform Memory Architecture) besser ausnutzen zu können, und zwar sowohl von Serversystemen als auch von SMP-Arbeitsstationen mit dem Intel i7 oder dem AMD Opteron. Die NUMA-Unterstützung im Speicher-Manager bietet Anwendungen und Treibern Informationen über die Knoten wie Standort, Topologie und Zugriffskosten, sodass sie NUMA-Fähigkeiten nutzen können, ohne sich um die Einzelheiten der Hardware kümmern zu müssen. Wenn der Speicher-Manager initialisiert wird, ruft er die Funktion MiComputerNumaCosts auf. Sie führt verschiedene Seiten- und Cacheoperationen auf verschiedenen Knoten aus und ermittelt, wie viel Zeit dafür jeweils erforderlich ist. Aufgrund dieser Informationen erstellt sie einen Graphen der Zugriffskosten (Abstand eines Knotens von allen anderen Knoten im System). Muss das System Seiten für eine bestimmte Operation anfordern, sucht es in dem Graphen nach dem optimalen (also dem nächstgelegenen) Knoten. Ist auf diesem Knoten kein Speicher verfügbar, wählt das System den zweitnächsten Knoten aus usw. Der Speicher-Manager sorgt zwar dafür, dass Speicherzuweisungen nach Möglichkeit auf dem Knoten mit dem idealen Prozessor für den Thread erfolgen, der die Zuweisung durchführt (also auf dem idealen Knoten), bietet aber auch die APIs wie VirtualAllocExNuma, CreateFileMappingNuma, MapViewOfFileExNuma und AllocateUserPhysicalPagesNuma, mit denen Anwendungen selbst einen Knoten auswählen können. Der ideale Knoten wird nicht nur dann verwendet, wenn Anwendungen Speicher zuweisen, sondern auch bei Kerneloperationen und Seitenfehlern. Wenn beispielsweise ein Thread, der nicht auf seinem idealen Prozessor läuft, einen Seitenfehler hervorruft, verwendet der Speicher-Manager nicht den aktuellen Knoten, sondern weist Speicher vom idealen Knoten des Threads zu. Das kann zwar zu einer längeren Zugriffszeit führen, wenn der Thread nach wie vor auf seiner nicht idealen CPU läuft, doch der Speicherzugriff wird insgesamt verbessert, wenn der Thread zu seinem idealen Knoten zurückkehrt. Stehen auf dem idealen Knoten keine Res- NUMA Kapitel 5 461
sourcen mehr zur Verfügung, wird ohnehin der ihm am nächsten gelegene Knoten ausgewählt und kein willkürlicher anderer Knoten. Ebenso wie Benutzermodusanwendungen können jedoch auch Treiber selbst einen Knoten auswählen, indem sie APIs wie MmAllocatePagesForMdlEx und MmAllocateContinuousMemorySpecifyCacheNode verwenden. Auch mehrere Pools und Datenstrukturen des Speicher-Managers sind zur Nutzung von NUMA-Knoten optimiert. Der Speicher-Manager versucht, den physischen Speicher von allen Knoten auf dem System gleichmäßig für den nicht ausgelagerten Pool zu nutzen. Bei Zuweisungen aus diesem Pool nutzt der Speicher-Manager den idealen Knoten als Index, um den virtuellen Adressbereich im nicht ausgelagerten Pool auszuwählen, der mit dem physischen Speicher dieses Knotens übereinstimmt. Außerdem wird für jeden NUMA-Knotenpool eine Liste der freien Seiten geführt, um diese Art von Speicherkonfiguration effizient nutzen zu können. Nicht nur der nicht ausgelagerte Pool, sondern auch der Systemcache, die System-PTEs und die Look-Aside-Listen des Speicher-Managers werden auf diese Weise über alle Knoten hinweg zugewiesen. Wenn das System Nullseiten benötigt, verteilt es sie ebenfalls über verschiedene NUMA-Knoten. Dazu werden Threads mit NUMA-Affinitäten für die Knoten erstellt, in denen sich der physische Speicher befindet. Der logische Prefetcher und SuperFetch (siehe den Abschnitt »Vorausschauende Speicherverwaltung (SuperFetch)«) verwenden ebenfalls den idealen Knoten des Zielprozesses. Weiche Seitenfehler können dazu führen, dass Seiten auf den idealen Knoten für den betroffenen Thread übertragen werden. Abschnittsobjekte Wie bereits weiter vorn in diesem Kapitel im Abschnitt »Gemeinsam genutzter Arbeitsspeicher und zugeordnete Dateien« beschrieben, ist das Speicherobjekt – oder Dateizuordnungsobjekt, wie es im Windows-Teilsystem genannt wird – ein Speicherblock, den mehrere Prozesse gemeinsam nutzen können. Es kann in der Auslagerungsdatei oder einer anderen Datei auf der Festplatte zugeordnet werden. Die Exekutive verwendet ebenfalls Abschnitte, um ausführbare Abbilder in den Speicher zu laden, und der Cache-Manager nutzt sie für den Zugriff auf die Daten in einer zwischengespeicherten Datei (mehr darüber in Kapitel 14 von Band 2). Mithilfe von Sitzungsobjekten können Sie auch eine Datei im Prozessadressraum zuordnen. Der Zugriff auf die Datei kann dann in Form eines großen Arrays erfolgen. Dazu werden die einzelnen Sichten des Abschnittsobjekts zugeordnet und die Lese- und Schreibvorgänge im Arbeitsspeicher statt in der Datei durchgeführt. Dieser Vorgang wird E/A in einer im Speicher abgebildeten Datei genannt. Wenn das Programm auf eine ungültige Seite zugreift (die sich nicht im physischen Speicher befindet), tritt ein Seitenfehler auf, woraufhin der Speicher-Manager diese Seite automatisch aus der zugeordneten Datei oder der Auslagerungsdatei in den Speicher holt. Falls die Anwendung die Seite ändert, schreibt der Speicher-Manager diese Änderungen während seiner normalen Auslagerungsoperationen in die Datei. (Alternativ kann die Anwendung eine Sicht auch ausdrücklich mithilfe der Windows-Funktion FlushViewOfFile auf die Festplatte übertragen.) Wie andere Objekte erfolgt auch die Zuweisung und Aufheben der Zuweisung von Abschnittsobjekten durch den Objekt-Manager. Er erstellt und initialisiert einen Objektheader, 462 Kapitel 5 Speicherverwaltung
den er zur Verwaltung der Objekte verwendet. Der Rumpf des Abschnittsobjekts dagegen wird vom Speicher-Manager definiert. (Mehr über den Objekt-Manager erfahren Sie in Kapitel 8 von Band 2.) Der Speicher-Manager implementiert auch Dienste, die Benutzermodusthreads aufrufen können, um die im Rumpf eines Abschnittsobjekts gespeicherten Attribute abzurufen und zu ändern. Abb. 5–31 zeigt den Aufbau eines Abschnittsobjekts. Die in Abschnittsobjekten gespeicherten Attribute finden Sie in Tabelle 5–14. Objekttyp Objektrumpfattribute Dienste Section Maximale Größe Seitenschutz Auslagerungsdatei oder zugeordnete Datei Mit oder ohne Basisadresse Create Section Open Section Extend Section Map/Unmap View Query Section Abbildung 5–31 Ein Abschnittsobjekt Attribut Bedeutung Maximale Größe Die Höchstgröße in Bytes, auf die der Abschnitt wachsen kann. Bei der Zuordnung einer Datei ist dies die Höchstgröße der Datei. Seitenschutz Der Speicherschutz, der allen Seiten des Abschnitts bei seiner Erstellung zugewiesen wird. Auslagerungsdatei oder zugeordnete Datei Gibt an, ob der Abschnitt leer erstellt wird (mit der Auslagerungsdatei als Hintergrundspeicher; wie bereits erklärt, können auslagerungsgepufferte Abschnitte nur dann Ressourcen der Auslagerungsdatei nutzen, wenn Seiten auf die Festplatte geschrieben werden müssen) oder mit einer geladenen Datei (mit einer zugeordneten Datei als Hintergrundspeicher). Mit oder ohne Basisadresse Gibt an, ob sich der Abschnitt für alle Prozesse, die ihn nutzen, an derselben virtuellen Adresse befinden muss, oder ob er für die einzelnen Prozesse jeweils an verschiedenen Adressen stehen kann. Tabelle 5–14 Rumpfattribute von Abschnittsobjekten Experiment: Abschnittsobjekte untersuchen Mit Process Explorer von Sysinternals können Sie sich die von einem Prozess zugeordneten Dateien ansehen. Gehen Sie dazu folgendermaßen vor: 1. Öffnen Sie das Menü View und wählen Sie Lower Pane View und dann DLLs. 2. Öffnen Sie das Menü View und wählen Sie Select Columns und dann DLLs und aktivieren Sie die Spalte Mapping Type. → Abschnittsobjekte Kapitel 5 463
3. Schauen Sie sich die Dateien an, die in der Spalte Mapping als Data gekennzeichnet sind. Dies sind zugeordnete Dateien und keine DLLs oder andere vom Abbildlader als Module geladenen Dateien. Abschnittsobjekte, die eine Auslagerungsdatei als Hintergrundspeicher verwenden, sind in der Spalte Name durch die Angabe <Pagefile ­Backed> gekennzeichnet. Bei anderen Abschnittsobjekten wird der Dateiname angezeigt. Eine andere Möglichkeit, sich die Abschnittsobjekte anzusehen, besteht darin, zur Handleansicht zu wechseln (öffnen Sie das Menü View und wählen Sie Lower Pane View und dann Handles) und darin nach Objekten vom Typ Section zu suchen. Im folgenden Screenshot wird dabei jeweils der Objektname angezeigt (falls vorhanden). Dies ist nicht der Name der Datei, die als Hintergrundspeicher des Abschnitts verwendet wird, sondern der Name, den der Abschnitt im Namensraum des Objekt-Managers hat (siehe Kapitel 8 von Band 2). Wenn Sie auf einen Eintrag doppelklicken, sehen Sie weitere Informationen über das Objekt, darunter die Anzahl der offenen Handles und den Sicherheitsdeskriptor. → 464 Kapitel 5 Speicherverwaltung
Abb. 5–32 zeigt die vom Speicher-Manager unterhaltenen Datenstrukturen zur Beschreibung von zugeordneten Abschnitten. Diese Strukturen sorgen dafür, dass die von den zugeordneten Dateien gelesenen Daten unabhängig von der Art des Zugriffs (offene Datei, zugeordnete Datei usw.) konsistent sind. Für jede offene Datei (dargestellt durch ein Dateiobjekt) gibt es eine einzige Struktur für Abschnittsobjektzeiger. Sie ist von entscheidender Bedeutung, um die Konsistenz der Daten für alle Arten des Dateizugriffs zu erhalten und um eine Zwischenspeicherung der Dateien zu ermöglichen. Diese Struktur zeigt auf einen oder zwei Steuerbereiche. Beide dienen dazu, die Datei zuzuordnen, wobei der eine verwendet wird, wenn auf die Datei als Datendatei zugegriffen wird, und der andere, wenn sie als Abbild ausgeführt wird. Die Steuerbereiche wiederum zeigen auf Teilabschnittsstrukturen, die die Zuordnungsinformationen für die einzelnen Abschnitte der Datei angeben (schreibgeschützt, Lesen/Schreiben, Kopieren beim Schreiben usw.). Außerdem zeigen die Steuerbereiche auf eine im ausgelagerten Pool zugewiesene Segmentstruktur, diese wiederum auf die Prototyp-PTEs zur Zuordnung der eigentlichen, vom Abschnittsobjekt zugeordneten Seiten. Wie in diesem Kapitel bereits beschrieben, zeigen die Prozessseitentabellen auf diese Prototyp-PTEs, die wiederum die Seiten zuordnen, auf die verwiesen wird. Abschnittsobjekte Kapitel 5 465
VAD Abschnitts­ objekt Dateiobjekt Abschnitts­ objekt­zeiger Segment Datensteuer­ bereich Teilabschnitt Dateiobjekt Abbildsteuerbereich (wenn es sich bei der Datei um ein ausführbares Abbild handelt) Nächster Teilabschnitt Prototyp-PTEs PFN-Daten­ bank­ein­trag Seiten­tabelle Abbildung 5–32 Interne Strukturen eines Abschnitts Windows sorgt zwar dafür, dass alle Prozesse, die auf eine Datei zugreifen (sie liest oder schreibt), stets konsistente Daten sehen, doch es ist möglich, dass sich zwei Kopien der Seiten einer Datei im Arbeitsspeicher befinden. In diesem Fall erhalten jedoch alle Prozesse, die auf die Datei zugreifen, die jüngste Kopie, sodass die Datenkonsistenz trotzdem sichergestellt ist. Diese Duplizierung kann vorkommen, wenn eine Abbilddatei als Datendatei gelesen oder geschrieben wurde und dann als Abbild ausgeführt wird. Das ist beispielsweise möglich, wenn das Abbild verlinkt und dann ausgeführt wird. Der Linker hat die Datei für den Datenzugriff geöffnet, und bei der Ausführung als Abbild hat der Abbildlader sie anschließend als ausführbare Datei zugeordnet. Intern geschieht dabei Folgendes: 1. Wenn die ausführbare Datei mithilfe der Dateizuordnungs-APIs oder des Cache-Managers erstellt wurde, wird ein Datensteuerbereich zur Darstellung der Datenseiten in der Abbilddatei angelegt, die gelesen oder geschrieben werden. 2. Wird das Abbild ausgeführt und das Abschnittsobjekt zur Abbildung des Abbilds als ausführbare Datei erstellt, erkennt der Speicher-Manager, dass das Abschnittsobjekt für die Abbilddatei auf einen Datensteuerbereich zeigt, und leert den Abschnitt auf die Festplatte. Das ist notwendig, damit jegliche geänderten Seiten auf die Festplatte geschrieben werden, bevor ein Zugriff auf das Abbild über den Abbildsteuerbereich erfolgt. 3. Der Speicher-Manager erstellt einen Steuerbereich für die Abbilddatei. 4. Wenn das Abbild ausgeführt wird, werden seine (schreibgeschützten) Seiten aus der Abbilddatei eingelagert oder direkt aus der Datendatei herüberkopiert, falls die zugehörige Datenseite im Speicher vorhanden ist. Da sich die vom Datensteuerbereich zugeordneten Seiten nach wie vor im Speicher (oder auf der Standbyliste) befinden können, ist dies der einzige Fall, in dem zwei Exemplare derselben Daten in zwei verschiedenen Seiten im Arbeitsspeicher vorhanden sein können. Allerdings führt diese Duplizierung nicht zu Inkonsistenzen, weil der Datensteuerbereich wie schon erwähnt 466 Kapitel 5 Speicherverwaltung
bereits auf die Festplatte geleert wurde, sodass die aus dem Abbild gelesenen Seiten auf dem neuesten Stand sind (und niemals wieder auf die Festplatte geschrieben werden). Experiment: Steuerbereiche einsehen Um die Adressen der Steuerbereichstrukturen für eine Datei zu finden, müssen Sie zunächst die Adresse des betreffenden Dateiobjekts ermitteln, indem Sie mit dem Kerneldebuggerbefehl !handle die Prozesshandletabelle ausgeben. Anschließend untersuchen Sie das Dateiobjekt mit dt, um die Adresse der Struktur mit den Abschnittsobjektzeigern zu ermitteln. (Der Befehl !file gibt zwar grundlegende Informationen über ein Dateiobjekt aus, aber nicht den Zeiger auf diese Struktur.) Diese Struktur enthält drei Zeiger: einen auf den Datensteuerbereich, einen auf die gemeinsam genutzte Cachezuordnung (siehe Kapitel 14 in Band 2) und einen auf den Abbildsteuerbereich. Wenn es also einen Steuerbereich für die Datei gibt, können Sie seine Adresse aus dieser Zeigerstruktur ermitteln und anschließend in den Befehl !ca einspeisen. Öffnen Sie eine PowerPoint-Datei und lassen Sie sich mit dem Befehl !handle die Handle­ tabelle für den zugehörigen Prozess anzeigen. Dabei finden Sie (mithilfe einer Textsuche) ein offenes Handle für die PowerPoint-Datei. (Weitere Informationen über die Verwendung von !handle finden Sie im Abschnitt »Der Objekt-Manager« in Kapitel 8 von Band 2 und in der Dokumentation des Debuggers.) lkd> !process 0 0 powerpnt.exe PROCESS ffffc8068913e080 SessionId: 1 Cid: 2b64 Peb: 01249000 ParentCid: 1d38 DirBase: 252e25000 ObjectTable: ffffda8f49269c40 HandleCount: 1915. Image: POWERPNT.EXE lkd> .process /p ffffc8068913e080 Implicit process is now ffffc806'8913e080 lkd> !handle ... 0c08: Object: ffffc8068f56a630 GrantedAccess: 00120089 Entry: ffffda8f491d0020 Object: ffffc8068f56a630 Type: (ffffc8068256cb00) File ObjectHeader: ffffc8068f56a600 (new version) HandleCount: 1 PointerCount: 30839 Directory Object: 00000000 Name: \WindowsInternals\7thEdition\ Chapter05\diagrams.pptx {HarddiskVolume2} ... Untersuchen Sie nun die Adresse des Dateiobjekts (FFFFC8068F56A630) mit dt: lkd> dt nt!_file_object ffffc8068f56a630 +0x000 Type : 0n5 +0x002 Size : 0n216 → Abschnittsobjekte Kapitel 5 467
+0x008 DeviceObject : 0xffffc806'8408cb40 _DEVICE_OBJECT +0x010 Vpb : 0xffffc806'82feba00 _VPB +0x018 FsContext : 0xffffda8f'5137cbd0 Void +0x020 FsContext2 : 0xffffda8f'4366d590 Void +0x028 SectionObjectPointer : 0xffffc806'8ec0c558 _SECTION_OBJECT_POINTERS ... Anschließend untersuchen Sie die Adresse der Struktur mit den Abschnittsobjektzeigern mit dt: lkd> dt nt!_section_object_pointers 0xffffc806'8ec0c558 +0x000 DataSectionObject : 0xffffc806'8e838c10 Void +0x008 SharedCacheMap : 0xffffc806'8d967bd0 Void +0x010 ImageSectionObject : (null) Nun können Sie mit dem Befehl !ca unter Angabe der gefundenen Adresse den Steuerbereich einsehen: lkd> !ca 0xffffc806'8e838c10 ControlArea @ ffffc8068e838c10 Segment ffffda8f4d97fdc0 Flink ffffc8068ecf97b8 Blink ffffc8068ecf97b8 Section Ref 1 Pfn Ref 58 Mapped Views 2 User Ref 0 WaitForDel 0 Flush Count 0 ModWriteCount 0 System Views 2 File Object ffffc8068e5d3d50 WritableRefs 0 Flags (8080) File WasPurged \WindowsInternalsBook\7thEdition\Chapter05\ diagrams.pptx Segment @ ffffda8f4d97fdc0 ControlArea ffffc8068e838c10 Total Ptes ExtendInfo 0000000000000000 Segment Size 80 80000 Committed 0 Flags (c0000) ProtectionMask Subsection 1 @ ffffc8068e838c90 ControlArea ffffc8068e838c10 Starting Sector 0 Base Pte Ptes In Subsect 58 Unused Ptes 0 0 Protection 6 Blink ffffc8068bb7fcf0 MappedViews 2 ControlArea ffffc8068e838c10 Starting Sector 58 Number Of Sectors 28 Base Pte Ptes In Subsect 28 Unused Ptes Flags ffffda8f48eb6d40 d Sector Offset Number Of Sectors 58 Accessed Flink ffffc8068bb7fcf0 Subsection 2 @ ffffc8068c2e05b0 ffffda8f3cc45000 1d8 → 468 Kapitel 5 Speicherverwaltung
Flags d Sector Offset 0 Protection 6 MappedViews 1 Accessed Flink ffffc8068c2e0600 Blink ffffc8068c2e0600 Eine weitere Möglichkeit besteht darin, sich mit !memusage eine Liste aller Steuerbereiche anzeigen zu lassen. Auf einem System mit sehr viel Arbeitsspeicher kann dieser Befehl sehr viel Zeit in Anspruch nehmen. Das folgende Listing stellt einen Auszug aus der Ausgabe dieses Befehls dar: lkd> !memusage loading PFN database loading (100% complete) Compiling memory usage data (99% Complete). Zeroed: 98533 ( 394132 kb) Free: 1405 ( 5620 kb) Standby: 331221 ( 1324884 kb) Modified: 83806 ( 335224 kb) ModifiedNoWrite: 116 ( 464 kb) Active/Valid: 1556154 ( 6224616 kb) Transition: 5 ( 20 kb) SLIST/Bad: 1614 ( 6456 kb) Unknown: 0 ( 0 kb) TOTAL: 2072854 ( 8291416 kb) Dangling Yes Commit: 130 ( 520 kb) Dangling No Commit: 514812 ( 2059248 kb) Building kernel map Finished building kernel map (Master1 0 for 1c0) (Master1 0 for e80) (Master1 0 for ec0) (Master1 0 for f00) Scanning PFN database - (02% complete) (Master1 0 for de80) Scanning PFN database - (100% complete) → Abschnittsobjekte Kapitel 5 469
Usage Summary (in Kb): Control Valid Standby Dirty Shared Locked PageTables name ffffffffd 1684540 0 0 0 1684540 0 AWE ffff8c0b7e4797d0 64 0 0 0 0 0 mapped_file( MicrosoftWindows-Kernel-PnP%4Configuration.evtx ) ffff8c0b7e481650 0 4 0 0 0 0 mapped_file( No name for file ) ffff8c0b7e493c00 0 40 0 0 0 0 mapped_file( FSD-{ED5680AF0543-4367-A331-850F30190B44}.FSD ) ffff8c0b7e4a1b30 8 12 0 0 0 0 mapped_file( msidle.dll ) ffff8c0b7e4a7c40 128 0 0 0 0 0 mapped_file( MicrosoftWindows-Diagnosis-PCW%4Operational.evtx ) ffff8c0b7e4a9010 16 8 0 16 0 0 mapped_file( netjoin.dll )8a04db00 ... ffff8c0b7f8cc360 8212 0 0 0 0 0 mapped_file( OUTLOOK.EXE ) ffff8c0b7f8cd1a0 52 28 0 0 0 0 mapped_file( verdanab.ttf ) ffff8c0b7f8ce910 0 4 0 0 0 0 mapped_file( No name for file ) ffff8c0b7f8d3590 0 4 0 0 0 0 mapped_file( No name for file ) ... Die Spalte Control zeigt auf die Steuerbereichstruktur, die die zugeordnete Datei beschreibt. Mit dem Kerneldebuggerbefehl !ca können Sie sich die Speicherbereiche, Segmente und Teilabschnitte anzeigen lassen. Um beispielsweise die Steuerbereiche für die zugeordnete Datei Outlook.exe in diesem Beispiel auszugeben, verwenden Sie den Befehl !ca gefolgt von der zugehörigen Nummer in der Spalte Control: lkd> !ca ffff8c0b7f8cc360 ControlArea @ ffff8c0b7f8cc360 Segment ffffdf08d8a55670 Flink ffff8c0b834f1fd0 Blink ffff8c0b834f1fd0 Section Ref 1 Pfn Ref User Ref 2 WaitForDel File Object ffff8c0b7f0e94e0 ModWriteCount WritableRefs 806 Mapped Views 1 0 Flush Count c5a0 0 System Views ffff 80000161 Flags (a0) Image File \Program Files (x86)\Microsoft Office\root\Office16\ OUTLOOK.EXE Segment @ ffffdf08d8a55670 ControlArea ffff8c0b7f8cc360 Total Ptes 1609 Segment Size 1609000 Image Commit f4 ProtoPtes BasedAddress 0000000000be0000 Committed Image Info 0 ffffdf08d8a556b8 ffffdf08dab6b000 Flags (c20000) ProtectionMask → 470 Kapitel 5 Speicherverwaltung
Subsection 1 @ ffff8c0b7f8cc3e0 ControlArea ffff8c0b7f8cc360 Starting Sector 0 Number Of Sectors 2 Base Pte Ptes In Subsect 1 Unused Ptes 0 Sector Offset Protection 1 Flags ffffdf08dab6b000 2 0 Subsection 2 @ ffff8c0b7f8cc418 ControlArea ffff8c0b7f8cc360 Starting Sector Base Pte Ptes In Subsect f63 Unused Ptes 0 Sector Offset Protection 3 Flags ffffdf08dab6b008 6 2 Number Of Sectors 7b17 0 Subsection 3 @ ffff8c0b7f8cc450 ControlArea ffff8c0b7f8cc360 Starting Sector 7b19 Number Of Sectors 19a4 Base Pte Ptes In Subsect Unused Ptes 0 Protection 1 Flags ffffdf08dab72b20 2 Sector Offset 335 0 Subsection 4 @ ffff8c0b7f8cc488 ControlArea ffff8c0b7f8cc360 Starting Sector 94bd Number Of Sectors 764 Base Pte Ptes In Subsect f2 Unused Ptes 0 0 Protection 5 Flags ffffdf08dab744c8 a Sector Offset Subsection 5 @ ffff8c0b7f8cc4c0 ControlArea ffff8c0b7f8cc360 Starting Sector 9c21 Number Of Sectors 1 Base Pte Ptes In Subsect 1 Unused Ptes 0 Sector Offset 0 Protection 5 Flags ffffdf08dab74c58 a Subsection 6 @ ffff8c0b7f8cc4f8 ControlArea ffff8c0b7f8cc360 Starting Sector 9c22 Number Of Sectors 1 Base Pte Ptes In Subsect 1 Unused Ptes 0 Sector Offset 0 Protection 5 Flags ffffdf08dab74c60 a Subsection 7 @ ffff8c0b7f8cc530 ControlArea ffff8c0b7f8cc360 Starting Sector 9c23 Number Of Sectors c62 Base Pte Ptes In Subsect Unused Ptes 0 Protection 1 Flags ffffdf08dab74c68 2 Sector Offset 18d 0 Subsection 8 @ ffff8c0b7f8cc568 ControlArea ffff8c0b7f8cc360 Starting Sector a885 Number Of Sectors 771 Base Pte Ptes In Subsect ef Unused Ptes 0 0 Protection 1 Flags ffffdf08dab758d0 2 Sector Offset Abschnittsobjekte Kapitel 5 471
Arbeitssätze Sie haben jetzt gesehen, wie Windows den Überblick über den physischen Arbeitsspeicher wahrt und wie viel Speicher es unterstützt. Als Nächstes beschäftigen wir uns damit, wie Windows einen Teil der virtuellen Adressen im physischen Speicher behält. Wie Sie wissen, wird der Teil der virtuellen Seiten, die sich im physischen Speicher befinden, als Arbeitssatz bezeichnet. Es gibt drei Arten davon: QQ Prozessarbeitssätze Sie enthalten die Seiten, auf die Threads innerhalb eines einzelnen Prozesses verwiesen haben. QQ Systemarbeitssätze Sie enthalten den speicherresidenten Teil des auslagerungsfähigen Systemcodes (z. B. Ntoskrnl.exe und Treiber), den ausgelagerten Pool und den Systemcache. QQ Sitzungsarbeitssätze Jede Sitzung verfügt über ihren eigenen Arbeitssatz mit dem speicherresidenten Teil der sitzungsspezifischen Kernelmodus-Datenstrukturen, die vom Kernelmodusteil des Windows-Teilsystems (Win32k.sys) zugewiesen wurden, dem ausgelagerten Pool der Sitzung, den zugeordneten Sichten der Sitzung und anderen Gerätetreibern im Sitzungsraum. Bevor wir näher auf die einzelnen Arten von Arbeitssätzen eingehen, schauen wir uns an, wie allgemein entschieden wird, welche Seiten in den physischen Arbeitsspeicher gebracht werden und wie lange sie dort bleiben sollen. Auslagerung bei Bedarf Der Speicher-Manager verwendet einen bedarfsgesteuerten Auslagerungsmechanismus mit Clustering, um Seiten in den Speicher zu laden. Wenn ein Thread einen Seitenfehler empfängt, lädt der Speicher-Manager die fehlende Seite sowie einige wenige der vorausgehenden und/ oder nachfolgenden Seiten in den Speicher. Dadurch wird versucht, die Menge der Auslagerungs-E/A-Vorgänge in einem Thread zu minimieren. Da Programme – insbesondere umfangreiche – immer in kleinen Regionen ihres Adressraums ausgeführt werden, verringert das Laden ganzer Cluster von virtuellen Seiten die Anzahl der Lesevorgänge auf der Festplatte. Bei Seitenfehlern aufgrund von Verweisen auf Datenseiten in Abbildern beträgt die Clustergröße drei Seiten, bei allen anderen Seitenfehlern sieben Seiten. Diese bedarfsgesteuerte Vorgehensweise kann jedoch zu vielen Seitenfehlern in einem Prozess führen, wenn die Threads mit der Ausführung beginnen oder sie wieder aufnehmen. Um den Start von Prozessen (und des Systems) zu optimieren, setzt Windows eine intelligente Prefetch-Engine ein, den logischen Prefetcher, den wir im nächsten Abschnitt vorstellen. Eine weitergehende Optimierung mit zusätzlichen Prefetchvorgängen wird von der Komponente SuperFetch durchgeführt, die weiter hinten in diesem Kapitel beschrieben wird. 472 Kapitel 5 Speicherverwaltung
Der logische Prefetcher und ReadyBoot Beim Start des Systems oder einer Anwendung werden gewöhnlich erst einige Seiten aus einem Teil einer Datei in den Speicher geholt, dann vielleicht aus einem weiter entfernten Teil derselben Datei, danach aus einer anderen Datei, anschließend möglicherweise aus einem Verzeichnis und schließlich wieder aus der ersten Datei. Dieses Herumspringen verlangsamt den Zugriff erheblich. Analysen haben gezeigt, dass die Suchzeiten auf der Festplatte ein entscheidender Faktor für einen langsamen Start des Systems und der Anwendungen darstellen. Durch einen Vorabruf von mehreren Seiten auf einmal lässt sich der Zugriff in eine vernünftigere Reihenfolge bringen, ohne dass dazu eine übermäßige Rückverfolgung erforderlich ist. Dadurch wird die erforderliche Zeit zum Hochfahren des Systems oder zum Starten einer Anwendung insgesamt verbessert. Aufgrund der starken Korrelation der Zugriffsvorgänge über mehrere Startvorgänge des Systems bzw. der Anwendung hinweg sind die benötigten Seiten im Voraus bekannt. Der Prefetcher versucht, den Startvorgang zu beschleunigen, indem er beobachtet, auf welche Daten und welchen Code beim Starten des Systems oder einer Anwendung zugegriffen wird, und diese Informationen zu Beginn des nächsten Startvorgangs nutzt, um Code und Daten zu lesen. Der Speicher-Manager benachrichtigt den Prefetchercode im Kernel über Seitenfehler – sowohl harte, bei denen die Daten von der Festplatte gelesen werden müssen, als auch weiche, bei denen sich die Daten bereits im Arbeitsspeicher befinden und nur zum Arbeitssatz des Prozesses hinzugefügt werden müssen. Der Prefetcher beobachtet die ersten zehn Sekunden des Starts einer Anwendung. Das Hochfahren des Systems verfolgt er standardmäßig bis 30 Sekunden nach dem Start der Benutzershell (gewöhnlich Explorer). Ist das nicht möglich, führt er seine Beobachtung bis 60 Sekunden nach der Initialisierung der Windows-Dienste oder insgesamt 120 Sekunden lang durch, wobei er nach dem zuerst abgelaufenen Zeitraum Schluss macht. Die im Kernel aufgezeichnete Ablaufverfolgung umfasst Seitenfehler für die Metadatendatei der NTFS-Masterdateitabelle (MFT), falls die Anwendung auf Dateien oder Verzeichnisse auf NTFS-Volumes zugreift, und für die Dateien und Verzeichnisse, auf die verwiesen wird. Nach der Erfassung der Ablaufverfolgung wartet der Prefetchercode auf Anforderungen von der Prefetcherkomponente des Dienstes SuperFetch (%SystemRoot%\System32\Sysmain.dll), der in einer Instanz von Svchost läuft. Dieser Dienst ist sowohl für den logischen Prefetcher im Kernel als auch für die SuperFetch-Komponente zuständig, mit der wir uns später noch beschäftigen werden. Der Prefetcher meldet das Ereignis \KernelObjects\PrefetchTracesReady, um den SuperFetch-Dienst darüber zu informieren, dass die Ablaufverfolgungsdaten jetzt abgefragt werden können. Das Prefetching beim Start des Systems oder von Anwendungen können Sie einund ausschalten, indem Sie den DWORD-Registrierungswert EnablePrefetcher unter dem Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters bearbeiten. Mit dem Wert 0 schalten Sie das Prefetching komplett ab. Mit 1 aktivieren Sie es nur für Anwendungen, mit 2 nur für den Systemstart und mit 3 sowohl für Anwendungen als auch für den Systemstart. Arbeitssätze Kapitel 5 473
Der Dienst SuperFetch (der den logischen Prefetcher beherbergt, auch wenn dieser eine eigene Komponente ist und nichts mit der eigentlichen SuperFetch-Funktionalität zu tun hat) ruft die interne Systemfunktion NtQuerySystemInformation auf, um die Daten der Ablaufverfolgung anzufordern. Der logische Prefetcher verarbeitet diese Daten, kombiniert sie mit zuvor erfassten Daten und schreibt sie in eine Datei im Ordner %SystemRoot%\Prefetch (siehe Abb. 5–33). Der Name dieser Datei besteht aus dem Namen der Anwendung, für die die Ablaufverfolgung aufgenommen wurde, gefolgt von einem Bindestrich, dem Hash des Dateipfads in hexadezimaler Form und schließlich der Endung .pf, beispielsweise NOTEPAD.EXE-9FB27C0E.pf. Abbildung 5–33 Der Ordner Prefetch Es gibt jedoch eine Ausnahme von dieser Regel für Dateinamen, nämlich bei Abbildern, die andere Komponenten beherbergen, etwa die Microsoft Management Console (%SystemRoot%\ System32\Mmc.exe), der Diensthostprozess (%SystemRoot%\System32\Svchost.exe), RunDLL (%SystemRoot%\System32\Rundll32.exe) und Dllhost (%SystemRoot%\System32\Dllhost.exe). Da bei diesen Anwendungen Add-On-Komponenten an der Befehlszeile angegeben werden, schließt der Prefetcher die Befehlszeile in den Hash ein. Daher gibt es für Aufrufe dieser Anwendungen mit unterschiedlichen Komponenten jeweils eigene Ablaufverfolgungen. Beim Hochfahren des Systems wird dagegen der Mechanismus ReadyBoot verwendet. Um die E/A-Operationen zu optimieren, führt er umfangreiche und effiziente E/A-Lesevorgänge durch und speichert die Daten im RAM. Wenn Systemkomponenten die Daten benötigen, erhalten sie diejenigen aus dem RAM. Das ist insbesondere bei mechanischen Festplatten von Vorteil, kann sich aber auch bei SSDs als nützlich erweisen. Informationen über die Dateien, die 474 Kapitel 5 Speicherverwaltung
vorab abgerufen werden sollten, werden nach dem Hochfahren im Unterverzeichnis ReadyBoot des Verzeichnisses Prefetch aus Abb. 5–33 gespeichert. Nach Abschluss des Startvorgangs werden die zwischengespeicherten Daten aus dem RAM gelöscht. Bei sehr schnellen SSDs ist ReadyBoot standardmäßig ausgeschaltet, da dieser Mechanismus hier, wenn überhaupt, nur sehr geringe Vorteile bietet. Beim Start des Systems oder einer Anwendung wird der Prefetcher aufgerufen. Er schaut nach, ob sich im Prefetch-Verzeichnis Ablaufverfolgungen für die vorliegende Situation befinden. Wenn ja, ruft er NTFS auf, um jegliche vorhandenen Verweise auf die MFT-Metadatendatei abzurufen, liest den Inhalt aller darin genannten Verzeichnisse und öffnet schließlich alle Dateien, auf die verwiesen wird. Anschließend ruft er die Funktion MmPrefetchPages des Speicher-Managers auf, um jegliche Daten und jeglichen Code einzulesen, die in der Ablaufverfolgung angegeben, aber noch nicht im Speicher vorhanden sind. Der Speicher-Manager führt all diese Lesevorgänge asynchron aus und wartet auf ihren Abschluss, bevor er den fortgesetzten Start der Anwendung gestattet. Experiment: Lese- und Schreibvorgänge in der Prefetchdatei beobachten Wenn Sie eine Ablaufverfolgung für einen Anwendungsstart mit Process Monitor von Sys­ internals auf einer Windows-Clientversion erfassen (auf den Serverversionen ist das Prefetching standardmäßig ausgeschaltet), können Sie beobachten, wie der Prefetcher nach der Prefetchdatei für die Anwendung sucht und ggf. darin liest und wie er etwa zehn Sekunden nach dem Start der Anwendung ein neues Exemplar dieser Datei schreibt. Die folgende Anzeige stammt von einem Start des Windows-Editors (Notepad), wobei der Filter Include auf prefetch gesetzt wurde, sodass Process Monitor nur Zugriffe auf das Verzeichnis %SystemRoot%\Prefetch zeigt: → Arbeitssätze Kapitel 5 475
In Zeile 0 bis 3 sehen Sie, dass die Prefetchdatei für Notepad im Kontext des Prozesses Notepad während seines Starts gelesen wird. In den Zeilen 7 bis 19, deren Zeitstempel eine Zeit zehn Sekunden nach den ersten vier Zeilen angibt, schreibt der Dienst SuperFetch, der im Kontext eines Svchost-Prozesses läuft, die aktualisierte Prefetchdatei. Um die Suchvorgänge noch weiter zu reduzieren, stellt der Dienst SuperFetch etwa alle drei Tage in Leerlaufzeiten eine Liste der Dateien und Verzeichnisse in der Reihenfolge zusammen, in der beim Start des Systems oder der Anwendung auf sie verwiesen wird, und speichert sie in %SystemRoot%\Prefetch\Layout.ini (siehe Abb. 5–34). Diese Liste enthält auch von SuperFetch beobachtete Dateien, auf die häufig zugegriffen wurde. Abbildung 5–34 Layoutdatei für die Defragmentierung Anschließend startet der Dienst die Systemdefragmentierung mit einer Befehlszeilenoption, die besagt, dass der Vorgang nicht vollständig, sondern nur aufgrund des Inhalts dieser Datei erfolgen soll. Der Defragmentierungsmechanismus sucht in jedem Volume nach einem zusammenhängenden Bereich, der groß genug für alle in der Liste genannten Dateien und Verzeichnisse auf diesem Volume ist, und verschiebt sie komplett in diesen Bereich, sodass sie eine nach der anderen dort gespeichert sind. Zukünftige Prefetchvorgänge laufen dadurch effizienter ab, da sich die Daten jetzt in der Lesereihenfolge auf der Festplatte befinden. Da die Anzahl der vorab abgerufenen Dateien gewöhnlich nur in die Hunderte geht, verläuft dieser Vorgang weit schneller als die komplette Defragmentierung des Volumes. 476 Kapitel 5 Speicherverwaltung
Platzierungsrichtlinien Wenn ein Thread einen Seitenfehler empfängt, muss der Speicher-Manager entscheiden, wo er die virtuelle Seite im physischen Speicher ablegt. Die Regeln, die die ideale Position bestimmen, werden als Platzierungsrichtlinie bezeichnet. Bei der Auswahl der Seitenframes berücksichtigt Windows die Größe der CPU-Speichercaches, um deren unnötige Zersplitterung zu vermeiden. Ist der physische Speicher bei Auftreten eines Seitenfehlers voll belegt, bestimmt Windows anhand einer Ersetzungsrichtlinie, welche virtuelle Seite aus dem Speicher entfernt werden soll, um Platz für die neue zu schaffen. Zu den gängigen Ersetzungsrichtlinien gehören LRU (Least Recently Used) und FIFO (First In, First Out). Für den LRU-Algorithmus (in seiner Implementierung in den meisten UNIX-Versionen auch Taktalgorithmus genannt) muss sich das virtuelle Speichersystem merken, wann eine Seite im Speicher verwendet wird. Wird ein neuer Seitenframe gebraucht, so wird die Seite, die am längsten nicht benutzt wurde, aus dem Arbeitssatz entfernt. Die FIFO-Algorithmus ist etwas einfacher: Bei ihm wird die Seite entfernt, die sich am längsten im physischen Speicher befindet, unabhängig davon, wie oft sie benutzt wurde. Es wird zwischen lokalen und globalen Ersetzungsrichtlinien unterschieden. Bei einer globalen Richtlinie kann bei einem Seitenfehler auf jeden beliebigen Seitenframe zurückgegriffen werden, auch wenn er zu einem anderen Prozess gehört. Beispielsweise wird bei einer globalen Ersetzungsrichtlinie mit FIFO-Algorithmus einfach die Seite freigegeben, die sich am längsten im physischen Speicher befindet. Bei einer lokalen Richtlinie wird diese Suche auf Seiten eingeschränkt, die zu dem Prozess gehören, in dem der Seitenfehler aufgetreten ist. Globale Ersetzungsrichtlinien können Prozesse anfällig für das Verhalten anderer Prozesse machen. So kann eine störrische Anwendung das gesamte Betriebssystem unterminieren, indem sie in allen Prozessen eine übermäßige Auslagerung hervorruft. Windows verwendet sowohl lokale als auch globale Ersetzungsrichtlinien. Wenn ein Arbeitssatz an seine Grenzen stößt oder aufgrund der Nachfrage nach physischem Speicher verkleinert werden muss, entfernt der Speicher-Manager aus ihm Seiten, bis genügend freie Seiten vorhanden sind. Verwaltung von Arbeitssätzen Jeder Prozess hat zu Anfang eine Mindestgröße von 50 und eine Höchstgröße von 345 Seiten für seinen Arbeitssatz. Diese Grenzwerte können Sie mit der Windows-Funktion SetProcessWorkingSetSize ändern, für die Sie das Recht SeIncreaseBasePriorityPrivilege benötigen. Sofern Sie den Prozess nicht auch so eingerichtet haben, dass er harte Grenzen für den Arbeitssatz verwendet, werden diese Grenzwerte jedoch ignoriert: Der Speicher-Manager erlaubt dem Prozess, auch über sein Maximum hinaus zu wachsen, wenn eine starke Auslagerung stattfindet und noch ausreichend Speicher zur Verfügung steht. Umgekehrt kann er den Arbeitssatz auch unter sein Minimum schrumpfen lassen, wenn keine Auslagerung stattfindet, der physische Speicher aber stark nachgefragt ist. Harte Grenzwerte können Sie mit der Funktion SetProcessWorkingSetSizeEx und dem Flag QUOTA_LIMITS_HARDWS_MAX_ENABLE festlegen, allerdings ist es fast immer besser, die Verwaltung des Arbeitssatzes dem System zu überlassen. Auf 32-Bit-Systemen kann die Maximalgröße des Arbeitssatzes nicht das systemweite Maximum überschreiten, das bei der Systeminitialisierung berechnet und in der Kernelvariablen Arbeitssätze Kapitel 5 477
MiMaximumWorkingSet gespeichert wurde. Auf x64-Systemen ist der virtuelle Adressraum so um- fangreich, dass die Menge des physischen Speichers den praktischen oberen Grenzwert bildet. Die Arbeitssatzhöchstgrößen für verschiedene Systeme sind in Tabelle 5–15 aufgeführt. Windows-Version Höchstgröße des Arbeitssatzes x86, ARM 2 GB – 64 KB (0x7FFF0000) x86-Versionen von Windows, gestartet mit ­increaseuserva 2 GB – 64 KB + Erweiterung des virtuellen Benutzeradressraums x64 (Windows 8, Server 2012) 8192 GB (8 TB) x64 (Windows 8.1, 10, Server 2012 R2, 2016) 128 TB Tabelle 5–15 Obergrenze für die Größe von Arbeitssätzen Wenn ein Seitenfehler auftritt, werden die Grenzwerte des Arbeitssatzes für den Prozess und die Menge an freiem Arbeitsspeicher auf dem System untersucht. Sofern die Bedingungen es zulassen, erlaubt der Speicher-Manager einem Prozess, seinen Arbeitssatz bis zum oberen Grenzwert zu erweitern (oder sogar darüber hinaus, wenn keine harten Grenzwerte festgelegt sind und genügend freie Seiten zur Verfügung stehen). Ist dagegen der Speicher knapp, ersetzt Windows bei Seitenfehlern lieber vorhandene Seiten im Arbeitssatz, anstatt neue hinzuzufügen. Windows schreibt geänderte Seiten auf die Festplatte, um Arbeitsspeicher verfügbar zu halten. Erfolgen solche Änderungen sehr rasch, so ist trotzdem mehr Speicher erforderlich, um die Speicheranforderungen zu erfüllen. Wenn der physische Speicher knapp wird, löst der Arbeitssatz-Manager – der im Kontext des Systemthreads für den Balance-Set-Manager ausgeführt wird (siehe den nächsten Abschnitt) – die automatische Verkleinerung des Arbeitssatzes aus, um den Betrag an verfügbarem freiem Speicher im System zu erhöhen. Mit der bereits erwähnten Funktion SetProcessWorkingSetSizeEx können Sie diese Verkleinerung auch für ihren eigenen Prozess auslösen, etwa nach der Prozessinitialisierung. Der Arbeitssatz-Manager untersucht den verfügbaren Speicher und entscheidet, ob und welche Arbeitssätze verkleinert werden müssen. Ist ausreichend Speicher vorhanden, berechnet der Arbeitssatz-Manager, wie viele Seiten bei Bedarf aus den Arbeitssätzen entfernt werden können. Wird eine Verkleinerung notwendig, wendet er sich den Arbeitssätzen zu, deren Größe noch über dem Minimum liegt. Außerdem passt er dynamisch die Abstände an, in denen er die Arbeitssätze untersucht, und bringt die für eine Verkleinerung der Arbeitssätze anstehenden Prozesse in eine optimale Reihenfolge. Beispielsweise werden Prozesse mit Seiten, auf die seit Längerem nicht mehr zugegriffen wurde, als Erstes betrachtet. Umfangreiche Prozesse, die sich schon länger im Leerlauf befinden, werden eher berücksichtigt als kleinere Prozesse, die häufiger laufen. Prozesse, die die Vordergrundanwendung ausführen, werden als Letzte in Betracht gezogen. Wenn der Arbeitssatz-Manager Prozesse findet, die mehr als die Mindestgröße für ihre Arbeitssätze verwenden, sucht er nach Seiten, die er daraus entfernen kann, um sie für andere Zwecke zur Verfügung zu stellen. Ist der Betrag an freiem Speicher dann immer noch zu niedrig, fährt der Arbeitssatz-Manager damit fort, Seiten aus den Prozessarbeitssätzen zu entfernen, bis er die Mindestanzahl von freien Seiten auf dem System erreicht hat. 478 Kapitel 5 Speicherverwaltung
Der Arbeitssatz-Manager entfernt nach Möglichkeit Seiten, auf die nicht erst kürzlich zugegriffen wurde. Um das herauszufinden, überprüft er das Zugriffsbit im Hardware-PTE. Ist das Bit nicht gesetzt, so wird die Seite gealtert. Dabei wird ein Zähler hochgesetzt, der angibt, dass seit der letzten Untersuchung des Arbeitssatzes für eine mögliche Kürzung nicht auf die Seite verwiesen wurde. Später werden anhand dieses Alters die Seiten ausgewählt, die aus dem Arbeitssatz entfernt werden sollten. Ist das Zugriffsbit im Hardware-PTE dagegen gesetzt, entfernt der Arbeitssatz-Manager es und geht zur nächsten Seite im Arbeitssatz über. Wenn der Arbeitssatz-Manager sich die Seite das nächste Mal ansieht, ist das Bit daher nicht gesetzt. Daraus kann der Manager ablesen, dass seit seiner letzten Untersuchung nicht auf die Seite zugegriffen wurde. Diese Suche nach zu entfernenden Seiten wird im ganzen Arbeitssatz fortgesetzt, bis entweder genügend viele Seiten entfernt wurden oder die Untersuchung wieder am Anfangspunkt angekommen ist. Bei der nächsten Verkleinerung des Arbeitssatzes wird sie dort wieder aufgenommen, wo sie beim letzten Mal abgebrochen wurde. Experiment: Größen von Prozessarbeitssätzen einsehen In der Leistungsüberwachung können Sie sich die Größe von Arbeitssätzen mit den in der folgenden Tabelle genannten Indikatoren ansehen. Auch mithilfe anderer Hilfsprogramme zur Einsichtnahme in Prozesse (wie dem Task-Manager und Process Explorer) können Sie die Größe von Prozessarbeitssätzen ermitteln. Leistungsindikator Beschreibung Prozess: Arbeitsseiten Gibt die aktuelle Größe des Arbeitssatzes für den ausgewählten Prozess in Byte an. Prozess: Arbeitssatz (max.) Gibt die bisherige Spitzengröße des Arbeitssatzes für den ausgewählten Prozess in Byte an. Prozess: Seitenfehler/s Gibt die Anzahl der Seitenfehler an, die pro Sekunde bei diesem Prozess auftreten. Den Gesamtwert für sämtliche Prozessarbeitssätze erhalten Sie jeweils, indem Sie im Instanzfeld der Leistungsüberwachung _Total auswählen. Diese Instanz ist jedoch kein echter Prozess, sondern gibt lediglich die Summe der prozessspezifischen Zähler für alle zurzeit auf dem System laufenden Prozesse an. Die angezeigte Summe ist größer als der RAM, was daran liegt, dass die Größen der einzelnen Prozessarbeitssätze auch gemeinsam genutzte Seiten einschließen. Wenn mehrere Prozesse dieselbe Seite verwenden, wird sie bei den Arbeitssätzen aller dieser Prozesse mitgezählt. Arbeitssätze Kapitel 5 479
Experiment: Arbeitssatz- und virtuelle Größe im Vergleich Weiter vorn in diesem Kapitel haben Sie mit TestLimit zwei Prozesse erstellt, von denen der eine seinen Arbeitsspeicher nur reserviert und der andere ihn privat zugesichert hatte. Die Unterschiede haben Sie sich mit Process Explorer angesehen. Jetzt erstellen wir einen dritten TestLimit-Prozess, der den Arbeitsspeicher nicht nur zusichert, sondern auch tatsächlich darauf zugreift, sodass er in den Arbeitssatz aufgenommen wird. Gehen Sie dazu wie folgt vor: 1. Erstellen Sie einen neuen TestLimit-Prozess: C:\Users\pavely>testlimit -d 1 -c 800 Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - www.sysinternals.com Process ID: 13008 Leaking private bytes with touch 1 MB at a time... Leaked 800 MB of private memory (800 MB total leaked). Lasterror: 0 The operation completed successfully. 2. Öffnen Sie Process Explorer. 3. Öffnen Sie das Menü View, wählen Sie Select Columns und klicken Sie auf die Registerkarte Process Memory. 4. Aktivieren Sie die Indikatoren Private Bytes, Virtual Size, Working Set Size, WS Shareable Bytes und WS Private Bytes. 5. Suchen Sie die drei Instanzen von TestLimit: Der neue TestLimit-Prozess ist der dritte, also derjenige mit der PID 13008. Er ist der einzige der drei, der tatsächlich auf den zugewiesenen Speicher zugreift, und damit auch der einzige, dessen Arbeitssatzgröße die Größe der Zuweisung widerspiegelt. → 480 Kapitel 5 Speicherverwaltung
Dieses Ergebnis ist jedoch nur bei Systemen möglich, auf denen genügend RAM zur Verfügung steht, um den Arbeitssatz so stark wachsen zu lassen. Selbst auf diesem System befinden sich nicht alle privaten Bytes (821.888 KB) im privaten Teil des Arbeitssatzes (WS Private). Ein kleiner Teil der privaten Seite wurde aufgrund von Ersetzungen aus dem Arbeitssatz entfernt oder noch nicht eingelagert. Experiment: Die Arbeitssatzliste im Debugger einsehen Mit dem Kerneldebuggerbefehl !wsle können Sie sich die einzelnen Einträge im Arbeitssatz ansehen. Das folgende Beispiel zeigt einen Ausschnitt aus der Ausgabe von WinDbg für die Arbeitssatzliste auf einem 32-Bit-System: lkd> !wsle 7 Working Set Instance @ c0802d50 Working Set Shared @ c0802e30 FirstFree f7d LastEntry 203d NonDirect 0 FirstDynamic 6 NextSlot 6 LastInitialized 2063 HashTable 0 HashTableSize 0 Reading the WSLE data ....................................................... ....... Virtual Address Age Locked ReferenceCount c0603009 0 0 1 c0602009 0 0 1 c0601009 0 0 1 c0600009 0 0 1 c0802d59 6 0 1 c0604019 0 0 1 c0800409 2 0 1 c0006209 1 0 1 77290a05 5 0 1 7739aa05 5 0 1 c0014209 1 0 1 c0004209 1 0 1 72a37805 4 0 1 b50409 2 0 1 b52809 4 0 1 → Arbeitssätze Kapitel 5 481
7731dc05 6 0 1 bbec09 6 0 1 bbfc09 6 0 1 6c801805 4 0 1 772a1405 2 0 1 944209 1 0 1 77316a05 5 0 1 773a4209 1 0 1 77317405 2 0 1 772d6605 3 0 1 a71409 2 0 1 c1d409 2 0 1 772d4a05 5 0 1 77342c05 6 0 1 6c80f605 3 0 1 77320405 2 0 1 77323205 1 0 1 77321405 2 0 1 7ffe0215 1 0 2 a5fc09 6 0 1 7735cc05 6 0 1 ... Einige der Einträge in der Arbeitssatzliste sind Seiten von Seitentabellen (diejenigen mit einer Adresse größer als 0xC0000000), einige gelten für System-DLLs (diejenigen im Bereich 0x7nnnnnnn) und einige für den Code von WinDbg.exe selbst. Der Balance-Set-Manager und der Swapper Die Erweiterung und Verkleinerung von Arbeitssätzen erfolgt im Kontext eines Systemthreads, der als Balance-Set-Manager bezeichnet und bei der Systeminitialisierung angelegt wird (Funktion KeBalanceSetManager). Obwohl er technisch zum Kernel gehört, ruft er den Arbeitssatz-Manager des Speicher-Managers auf (MmWorkingSetManager), um die Analyse und Anpassung der Arbeitssätze durchzuführen. Der Balance-Set-Manager wartet auf zwei Ereignisobjekte: Das eine dieser Ereignisse wird gemeldet, wenn ein regelmäßiger Timer einmal pro Sekunde abläuft, das andere signalisiert der Speicher-Manager, wenn er feststellt, dass ein Arbeitssatz verändert werden muss. Treten beispielsweise sehr viele Seitenfehler auf oder ist die Liste der freien Seiten zu klein, weckt der Speicher-Manager den Balance-Set-Manager, sodass dieser den Arbeitssatz-Manager aufruft, um mit der Verkleinerung von Arbeitssätzen zu beginnen. Ist reichlich Speicher vorhanden, erlaubt der Arbeitssatz-Manager Prozessen mit Seitenfehlern, die Größe ihrer Arbeitssätze nach und nach zu erhöhen, indem sie die fehlenden Seiten in den Speicher zurückholen. Dabei wachsen die Arbeitssätze aber nur nach Bedarf. 482 Kapitel 5 Speicherverwaltung
Wenn der Balance-Set-Manager aufwacht, weil der 1-Sekunden-Timer abgelaufen ist, führt er folgende Aufgaben aus: 1. Wenn das System den sicheren virtuellen Modus (VSM) unterstützt (was auf Windows 10 und Windows Server 2016 der Fall ist), wird der sichere Kernel aufgerufen, um seine regelmäßigen Haushaltungsaufgaben zu erledigen (VslSecureKernelPeriodicTick). 2. Der Balance-Set-Manager ruft eine Routine zur Anpassung der IRP-Credits auf, um die Nutzung der prozesserweisen Look-Aside-Listen bei der IRP-Vervollständigung zu optimieren (IoAdjustIrpCredits). Das verbessert die Skalierbarkeit, wenn einzelne Prozessoren einer starken E/A-Last unterliegen. (Mehr über IRPs erfahren Sie in Kapitel 6.) 3. Er überprüft die Look-Aside-Liste und passt ggf. ihre Tiefe an, um die Zugriffszeit zu verbessern und Poolnutzung sowie Poolfragmentierung zu verringern (ExAdjustLookaside­ Depth). 4. Er passt die Größe des ETW-Pufferpools (Event Tracing for Windows) an, damit der ETW-Speicherpuffer wirtschaftlicher genutzt werden kann (EtwAdjustTraceBuffers). (Mehr über ETW erfahren Sie in Kapitel 8 von Band 2.) 5. Er ruft den Arbeitssatz-Manager des Speicher-Managers auf. Der Arbeitssatz-Manager verfügt über seine eigenen internen Indikatoren, um festzulegen, wann und wie stark Arbeitssätze verkleinert werden müssen. 6. Er setzt die Grenzwerte für die Ausführungszeit für Jobs durch (PsEnforceExecutionLimits). 7. Wenn der Balance-Set-Manager zum achten Mal erwacht, weil der 1-Sekunden-Timer abgelaufen ist, meldet er ein Ereignis, das einen weiteren Systemthread aufweckt, nämlich den Swapper (KeSwapProcessorStack). Der Swapper wird mit Priorität 23 ausgeführt und versucht, die Kernelstacks von Threads, die längere Zeit nicht ausgeführt wurden, auszulagern. Dazu sucht er nach Threads, die sich schon seit 15 Sekunden in einem Benutzermodus-Wartezustand befinden. Wenn er einen solchen Thread findet, versetzt er dessen Kernelstack in den Übergangsstatus (das heißt, er verschiebt die Seiten auf die Standbyliste oder die Liste der geänderten Seiten), um den physischen Speicher dafür zurückzugewinnen – getreu dem Grundsatz, dass ein Thread, der schon so lange gewartet hat, noch länger wartet. Wenn der letzte Kernelstack des letzten Threads in einem Prozess aus dem Arbeitsspeicher entfernt wurde, wird der Prozess für die komplette Auslagerung gekennzeichnet. Das ist der Grund dafür, dass Prozesse, die sich schon lange im Leerlauf befinden (wie Wininit oder Winlogon) eine Arbeitssatzgröße von 0 aufweisen können. Systemarbeitssätze Ebenso wie bei Prozessen werden auch der auslagerungsfähige Code und die auslagerungsfähigen Daten im Systemadressraum mithilfe von Arbeitssätzen verwaltet, wobei es insgesamt drei solcher globalen Systemarbeitssätze gibt: QQ Arbeitssatz des Systemcache Enthält die Seiten, die sich im Systemcache befinden. QQ Arbeitssatz des ausgelagerten Pools Enthält die Seiten, die sich im ausgelagerten Pool befinden. Arbeitssätze Kapitel 5 483
QQ Arbeitssatz der System-PTEs Enthält auslagerungsfähigen Code und auslagerungsfähige Daten der geladenen Treiber sowie das Kernelabbild und die Seiten von Abschnitten, die im Systemraum abgebildet wurden. Tabelle 5–16 gibt an, wo diese Systemarbeitssätze gespeichert sind. Arbeitssatz Speicherort (Windows 8.x, Windows Server 2012/R2) Speicherort (Windows 10, Windows Server 2016) Systemcache MmSystemCacheWs MiState.SystemVa.SystemWs[0] Ausgelagerter Pool MmPagedPoolWs MiState.SystemVa.SystemWs[2] System-PTEs MmSystemPtesWs MiState.SystemVa.SystemWs[1] Tabelle 5–16 Systemarbeitssätze Die Größen dieser Arbeitssätze bzw. die Größen der Komponenten, die zu ihnen beitragen, können Sie mit den in Tabelle 5–17 aufgeführten Leistungsindikatoren und Systemvariablen untersuchen. Die Leistungsindikatoren geben die Werte in Bytes an, die Systemvariablen dagegen in Seiten. Leistungsindikator (in Bytes) Systemvariabel (in Seiten) Bedeutung Arbeitsspeicher: Cache­bytes Arbeitsspeicher: Systemcache: Residente Bytes WorkingSetSize Der vom Dateisystemcache verbrauchte physische Arbeitsspeicher Arbeitsspeicher: Cachebytes (max.) PeakWorkingSetSize (Windows 10 und Windows Server 2016) Peak (Windows 8.x und Windows Server 2012/R2) Spitzengröße des Systemarbeitssatzes Arbeitsspeicher: Systemtreiber: Residente Bytes SystemPageCounts.­ SystemDriverPage (global, Windows 10 und Windows Server 2016) MmSystemDriverPage (global, Windows 8.x und Windows Server 2012/R2) Der von auslagerungsfähigem Gerätetreibercode verbrauchte physische Arbeitsspeicher Arbeitsspeicher: Auslagerungsseiten: Residente Bytes WorkingSetSize Der vom ausgelagerten Pool verbrauchte physische Arbeitsspeicher Tabelle 5–17 Leistungsindikatoren und Systemvariablen für Systemarbeitssätze Die Auslagerungsaktivität im Systemcache-Arbeitssatz können Sie sich mit dem Leistungsindikator Arbeitsspeicher: Cachefehler/s ansehen. Er gibt an, wie viele (harte und weiche) Seitenfehler im Systemcache-Arbeitssatz auftreten. Das Element PageFaultCount in der Struktur für den Systemcache-Arbeitssatz enthält den Wert dieses Indikators. 484 Kapitel 5 Speicherverwaltung
Speicherbenachrichtigungsereignisse Windows bietet eine Möglichkeit, um Benutzermodusprozesse und Kernelmodustreiber zu benachrichtigen, wenn der physische Speicher, der ausgelagerte Pool, der nicht ausgelagerte Pool oder der gesamte zugesicherte Speicher knapp bzw. reichlich vorhanden ist. Diese Informationen können dann herangezogen werden, um die Nutzung des Speichers angemessen zu gestalten. Ist beispielsweise wenig Speicher vorhanden, kann die Anwendung den Speicherverbrauch verringern. Ist der verfügbare ausgelagerte Pool sehr groß, kann ein Treiber mehr Speicher zuweisen. Mit einem weiteren Ereignis des Speicher-Managers sind auch Benachrichtigungen darüber möglich, dass beschädigte Seiten entdeckt wurden. Benutzermodusprozesse können nur darüber informiert werden, dass der Speicher knapp oder reichlich vorhanden ist. Eine Anwendung kann die Funktion CreateMemoryResourceNotification aufrufen und dabei angeben, ob sie Benachrichtigungen über niedrige oder hohe Verfügbarkeit des Speichers erhalten soll. Das zurückgegebene Handle kann an beliebige Wartefunktionen übergeben werden. Ist der Speicher knapp (oder reichlich), wird der Wartevorgang beendet, wodurch der Thread über die Speicherverfügbarkeit informiert wird. Die Speicherverfügbarkeit auf dem System kann auch jederzeit mit QueryMemoryResourceNotification abgefragt werden, ohne den aufrufenden Thread zu blockieren. Treiber verwenden dagegen den Ereignisnamen, den der Speichermanager im Verzeichnis \KernelObjects des Objekt-Managers eingerichtet hat, da Benachrichtigungen dadurch zustande kommen, dass der Speicher-Manager eines der von ihm definierten global benannten Ereignisobjekte aus Tabelle 5–18 ausgibt. Wird ein definierter Speicherzustand entdeckt, so wird das entsprechende Signal ausgegeben und dadurch jeder wartende Thread aufgeweckt. Ereignis Beschreibung HighCommitCondition Dieses Ereignis wird ausgelöst, wenn sich der gesamte zugesicherte Speicher dem oberen Grenzwert nähert, wenn also die Speichernutzung sehr hoch ist, sehr wenig Platz im physischen Speicher oder den Auslagerungsdateien vorhanden ist und das Betriebssystem die Auslagerungsdateien nicht erweitern kann. HighMemoryCondition Dieses Ereignis wird ausgelöst, wenn die Menge des freien physischen Speichers einen definierten Schwellenwert übersteigt. HighNonPagedPoolCondition Dieses Ereignis wird ausgelöst, wenn der Umfang des nicht ausgelagerten Pools einen definierten Schwellenwert übersteigt. HighPagedPoolCondition Dieses Ereignis wird ausgelöst, wenn der Umfang des ausgelagerten Pools einen definierten Schwellenwert übersteigt. LowCommitCondition Dieses Ereignis wird ausgelöst, wenn der gesamte zugesicherte Speicher im Vergleich zum aktuellen oberen Grenzwert gering ist, wenn also die Speichernutzung niedrig und viel Platz im physischen Speicher oder den Auslagerungsdateien vorhanden ist. LowMemoryCondition Dieses Ereignis wird ausgelöst, wenn die Menge des freien physischen Speichers einen definierten Schwellenwert unterschreitet. LowNonPagedPoolCondition Dieses Ereignis wird ausgelöst, wenn der Umfang des nicht ausgelagerten Pools einen definierten Schwellenwert unterschreitet. LowPagedPoolCondition Dieses Ereignis wird ausgelöst, wenn der Umfang des ausgelagerten Pools einen definierten Schwellenwert unterschreitet. Arbeitssätze Kapitel 5 → 485
Ereignis Beschreibung MaximumCommitCondition Dieses Ereignis wird ausgelöst, wenn sich der gesamte zugesicherte Speicher dem oberen Grenzwert nähert, wenn also die Speichernutzung sehr hoch ist, sehr wenig Platz im physischen Speicher oder den Auslagerungsdateien vorhanden ist und das Betriebssystem die Größe und Anzahl der Auslagerungsdateien nicht erhöhen kann. MemoryErrors Dieses Ereignis gibt an, dass eine unzulässige Seite (eine nicht mit Nullen gefüllte Nullseite) entdeckt wurde. Tabelle 5–18 Benachrichtigungsereignisse des Speicher-Managers Um die Standardeinstellung für den unteren oder oberen Schwellenwert zu überschreiben, fügen Sie den DWORD-Wert LowMemoryThreshold bzw. HighMemoryThresh­ old zum Registrierungsschlüssel HKLM\SYSTEM\CurrentControlSet\Session Manager\ Memory Management hinzu und setzen ihn auf den gewünschten Wert in Megabyte. Sie können auch dafür sorgen, dass das System beim Erkennen einer unzulässigen Seite kein Speicherfehlerereignis sendet, sondern abstürzt, indem Sie den DWORD-­ Registrierungswert PageValidationAction unter demselben Schlüssel auf 1 setzen. Experiment: Benachrichtigungsereignisse über Speicherressourcen einsehen Um sich die Benachrichtigungsereignisse über Speicherressourcen anzusehen, führen Sie das Sysinternals-Programm WinObj aus und klicken auf den Ordner KernelObjects. Daraufhin werden im rechten Bereich die Ereignisse für niedrigen und hohen Speicher angezeigt: → 486 Kapitel 5 Speicherverwaltung
Wenn Sie auf eines dieser Ereignisse klicken, können Sie sehen, wie viele Handles oder Verweise auf die Objekte erfolgt sind. Um zu ermitteln, ob Prozesse in dem System Benachrichtigungen über Speicherressourcen angefordert haben, durchsuchen Sie die Handletabelle nach Verweisen auf LowMemoryCondition oder HighMemoryCondition. Das können Sie über das Menü Find von Process Explorer tun (wählen Sie darin Find Handle or DLL) und mit WinDbg. (Eine Beschreibung der Handletabelle finden Sie im Abschnitt »Der Objekt-Manager« in Kapitel 8 von Band 2.) Die PFN-Datenbank Bislang haben wir uns häufiger mit der virtuellen Sicht eines Windows-Prozesses beschäftigt – mit den Seitentabellen, PTEs und VADs. Im Rest dieses Kapitels wollen wir uns ansehen, wie Windows den physischen Speicher verwaltet. Dabei beginnen wir damit, wie Windows den Überblick über diesen physischen Speicher behält. Während Arbeitssätze die im Speicher befindlichen Seiten eines Prozesses oder des Systems beschreiben, gibt die PFN-Datenbank (Page Frame Number) den Zustand jeder einzelnen Seite im physischen Speicher an. Die möglichen Zustände sind in Tabelle 5–19 aufgeführt. Status Beschreibung Aktiv oder gültig (Active/ valid) Die Seite gehört zu einem Prozess-, Sitzungs- oder Systemarbeitssatz oder zu gar keinem Arbeitssatz (z. B. eine nicht ausgelagerte Kernelseite). Gewöhnlich zeigt ein gültiger PTE darauf. Übergang (Transition) Dies ist ein vorübergehender Zustand für eine Seite, die nicht zu einem Arbeitssatz gehört und sich auch nicht auf einer Seitenliste befindet. Eine Seite nimmt diesen Zustand ein, wenn auf ihr eine E/A stattfindet. Der PTE ist angegeben, sodass Seitenfehlerkollisionen erkannt und ordnungsgemäß gehandhabt werden können. (Diese Bedeutung des Begriffs »Übergang« unterscheidet sich von seiner Verwendung im Abschnitt über ungültige PTEs. Ein ungültiger Übergangs-PTE zeigt auf eine Seite, die sich auf der Standbyliste oder der Liste geänderter Seiten befindet.) Standby Die Seite gehörte zu einem Arbeitssatz, wurde aber daraus entfernt oder wurde direkt durch Prefetching oder Clustering in die Standbyliste aufgenommen. Seit sie zum letzten Mal auf die Festplatte geschrieben wurde, ist sie nicht geändert worden. Der PTE verweist immer noch auf die physische Seite, ist aber als ungültig und im Übergang befindlich gekennzeichnet. Geändert (Modified) Die Seite gehörte zu einem Arbeitssatz, wurde aber daraus entfernt. Während sie in Gebrauch war, wurde sie geändert, und ihre aktuellen Inhalte wurden noch nicht auf die Festplatte oder einen Remotespeicher geschrieben. Der PTE verweist immer noch auf die physische Seite, ist aber als ungültig und im Übergang befindlich gekennzeichnet. Die Seite muss in den Hintergrundspeicher geschrieben werden, bevor die physische Seite wiederverwendet werden kann. → Die PFN-Datenbank Kapitel 5 487
Status Beschreibung Geändert, nicht schreibbar (Modified no-write) Dieser Zustand ist gleich dem einer geänderten Seite, allerdings ist sie so gekennzeichnet, dass der Schreibthread für geänderte Seiten sie nicht auf die Festplatte schreiben kann. Der Cache-Manager nimmt die Kennzeichnung als geändert und nicht schreibbar auf Anforderung der Dateisystemtreiber vor. Beispielsweise verwendet NTFS diesen Status für Seiten mit Dateisystemmetadaten, damit es erst die Einträge des Transaktionsprotokolls auf die Festplatte leeren kann, bevor die Seiten, die durch diese Einträge geschützt sind, auf die Festplatte geschrieben werden. (NTFS-Transaktionsprotokolle werden in Kapitel 13 von Band 2 erklärt.) Frei (Free) Die Seite ist frei, enthält aber nicht genau bestimmte modifizierte Daten. Aus Sicherheitsgründen müssen diese Seiten zunächst mit Nullen initialisiert oder mit neuen Daten (z. B. aus einer Datei) überschrieben werden, bevor sie als Benutzerseiten an Benutzerprozesse übergeben werden können. Mit Null initialisiert (Zeroed) Die Seite ist frei und wurde vom Nullseitenthread mit Nullen überschrieben oder als eine Seite erkannt, die bereits Nullen enthält. ROM Die Seite steht für schreibgeschützten Speicher (Read-Only Memory). Unzulässig (Bad) Die Seite hat einen Paritäts- oder einen anderen Hardwarefehler hervorgerufen und kann nicht (oder nur als Teil einer Enklave) benutzt werden. Tabelle 5–19 Mögliche Zustände von physischen Seiten Die PFN-Datenbank besteht aus einem Array von Strukturen, die für die einzelnen physischen Speicherseiten auf dem System stehen. Abb. 5–35 zeigt diese Datenbank und ihre Beziehung zu den Seitentabellen. Wie Sie dort erkennen können, zeigen gültige PTEs gewöhnlich auf Einträge in der PFN-Datenbank (und der PFN-Index auf die entsprechende Seite im physischen Speicher), während die Einträge in der PFN-Datenbank für Nicht-Prototyp-PTEs ggf. zurück auf die Seitentabelle zeigen, die sie verwendet. Datenbankeinträge für Prototyp-PTEs dagegen zeigen zurück zu dem Prototyp-PTE. 488 Kapitel 5 Speicherverwaltung
Seitentabelle von Prozess 1 Gültig PFN-Datenbank Ungültig: Festplattenzugriff Ungültig: Übergang In Verwendung Standbyliste Seitentabelle von Prozess 2 Gültig In Verwendung Ungültig: Festplattenzugriff In Verwendung Gültig Seitentabelle von Prozess 3 Liste geänderter Seiten Gültig Ungültig: Übergang Ungültig: Festplattenzugriff Vorwärtszeiger Rückwärtszeiger Abbildung 5–35 Seitentabellen und die PFN-Datenbank Für sechs der Seitenzustände in Tabelle 5–19 gibt es verknüpfte Listen, sodass der Speicher-­ Manager Seiten des entsprechenden Typs schnell finden kann. (Aktive/gültige Seiten, Übergangsseiten und überladene »unzulässige« Seiten werden auf keiner systemweiten Liste geführt.) Der Status Standby ist darüber hinaus mit acht nach Priorität gegliederten Listen verknüpft. (Mit der Priorität beschäftigen wir uns weiter hinten in diesem Abschnitt noch genauer.) Abb. 5–36 zeigt ein Beispiel für die Verknüpfungen zwischen diesen Einträgen. Die PFN-Datenbank Kapitel 5 489
Mit Null initialisiert Aktiv Frei Standby Unzulässig Aktiv Geändert Aktiv ROM Geändert, nicht schreibbar Abbildung 5–36 Seitenlisten in der PFN-Datenbank Experiment: Die PFN-Datenbank einsehen Mit dem Programm MemInfo von der Website zu diesem Buch können Sie bei Angabe des Schalters -s die Größe der einzelnen Seitenlisten ausgeben: C:\Tools>MemInfo.exe -s MemInfo v3.00 - Show PFN database information Copyright (C) 2007-2016 Alex Ionescu www.alex-ionescu.com Initializing PFN Database... Done PFN Database List Statistics Zeroed: 4867 ( 19468 kb) Free: 3076 ( 12304 kb) Standby: 4669104 (18676416 kb) Modified: 7845 ( 31380 kb) ModifiedNoWrite: 117 ( 468 kb) Active/Valid: 3677990 (14711960 kb) Transition: 5 ( 20 kb) → 490 Kapitel 5 Speicherverwaltung
Bad: 0 ( 0 kb) Unknown: 1277 ( 5108 kb) TOTAL: 8364281 (33457124 kb) Ähnliche Informationen können Sie auch mit dem Kerneldebuggerbefehl !memusage gewinnen, allerdings erfordert das erheblich mehr Zeit. Seitenlistendynamik Abb. 5–37 zeigt ein Statusdiagramm für Seitenframeübergänge. Der Einfachheit halber wurden die Listen für »geändert, nicht schreibbar«, »unzulässig« und »ROM« weggelassen. Null­forderungs­ seiten­fehler Seiten­lese­vor­gänge von der Festplatte oder Kernel­zuwei­sungen Standbyliste Liste freier Leisten Prozess­arbeits­ satz Weiche Seiten­ fehler Ersetzung im Arbeitssatz Null­ seiten­ thread Null­ seiten­ liste Schreib­ thread für geänderte Seiten Liste geänderter Seiten Ersetzung im Arbeitssatz Private Bytes bei Prozessende Abbildung 5–37 Statusdiagramm für physische Seiten Seitenframes bewegen sich wie folgt von einer Liste zur anderen: QQ Wenn der Speicher-Manager eine mit Nullen initialisierte Seite benötigt, um auf einen Nullforderungsseitenfehler zu reagieren (einen Verweis auf eine Seite, die komplett null sein soll, oder auf eine zugesicherte, private Benutzermodusseite, auf die noch nie zugegriffen wurde), versucht er zunächst, sie von der Nullseitenliste abzurufen. Ist diese Liste leer, verwendet er eine Seite von der Liste der freien Seiten und initialisiert sie mit Nullen. Ist auch die Liste der freien Seiten leer, greift er auf eine Seite von der Standbyliste zurück. Die PFN-Datenbank Kapitel 5 491
Ein Grund für die Notwendigkeit einer mit Nullen initialisierten Seite können Sicherheitsanforderungen wie Common Criteria (CC) sein. Die meisten CC-Profile verlangen, dass Benutzermodusprozesse initialisierte Seitenframes bekommen, damit sie keine Speicher­ inhalte des vorherigen Prozesses lesen können. Daher gibt der Speicher-Manager Benutzermodusprozessen mit Nullen initialisierte Frames; es sei denn, dass die Seite vom Hintergrundspeicher gelesen wurde. In letzterem Fall verwendet der Speicher-Manager bevorzugt einen nicht mit Nullen gefüllten Frame, den er dann mit den Daten von der Festplatte oder dem Remotespeicher initialisiert. Die Nullseitenliste wird vom Nullseiten-Systemthread (Thread 0 im Systemprozess) mit Einträgen aus der Liste der freien Seiten gefüllt. Um mit seiner Arbeit zu beginnen, wartet der Nullseitenthread auf das Signal eines Gate-Objekts. Es wird ausgelöst, wenn die Liste der freien Seiten acht oder mehr Seiten enthält. Allerdings wird der Nullseitenthread nur dann ausgeführt, wenn auf mindestens einem Prozessor keine anderen Threads laufen, da er die Priorität 0 hat und ein Benutzerthread mindestens die Priorität 1. Wenn ein Treiber mithilfe von MmAllocatePagesForMdl(Ex) oder eine Windows-Anwendung mit AllocatePhysicalPages oder AllcoateUserPhysicalPagesNuma eine physische Seite zuweist oder wenn eine Anwendung große Seiten zuweist und dadurch Speicher mit Nullen überschrieben werden muss, verwendet der Speicher-Manager die Funktion MiZeroInParallel, um eine bessere Leistung zu erzielen. Sie ordnet größere Regionen zu als der Nullseitenthread, der immer nur eine Seite auf einmal bearbeitet. Auf Mehrprozessorsystemen erstellt der Speicher-Manager außerdem zusätzliche Systemthreads, um das Überschreiben mit Nullen parallel durchzuführen (bzw. um es auf NUMA-Plattformen für NUMA zu optimieren). QQ Braucht der Speicher-Manager keine mit Nullen initialisierte Seite, wendet er sich als Erstes an die Liste der freien Seite. Wenn diese leer ist, geht er zur Nullseitenliste über, und wenn auch diese leer ist, zu den Standbylisten. Bevor er Seitenframes von Standbylisten verwenden kann, muss er jedoch zunächst die Verweise vom ungültigen PTE (oder Prototyp-PTE) zurückverfolgen und entfernen, die auf diese Frames zeigen. Da die Einträge in der PFN-Datenbank Zeiger zurück auf die Seiten mit der Seitentabelle des vorherigen Benutzers enthalten (oder auf die Seite eines Prototyp-PTE-Pools für gemeinsam genutzte Seiten), kann der Speicher-Manager den PTE schnell finden und die entsprechenden Änderungen vornehmen. QQ Wenn ein Prozess eine Seite aus seinem Arbeitssatz entfernen muss, weil er auf eine neue Seite verweist und sein Arbeitssatz voll ist oder weil der Speicher-Manager den Arbeitssatz verkleinert, wird die Seite auf die Standbyliste gesetzt, sofern sie während ihres Aufenthalts im Speicher nicht modifiziert wurde, oder auf die Liste der geänderten Seiten. QQ Wird ein Prozess beendet, werden alle seine privaten Seiten auf die Liste der freien Seiten gesetzt. Wenn der letzte Verweis auf einen auslagerungsgepufferten Abschnitt geschlossen wird und dieser Abschnitt keine zugeordneten Sichten mehr enthält, werden auch diese Seiten auf die Liste der freien Seiten übertragen. 492 Kapitel 5 Speicherverwaltung
Experiment: Die Nullseitenliste und die Liste der freien Seiten Die Freigabe privater Seiten bei der Beendigung eines Prozesses können Sie sich im Dialogfeld System Information von Process Explorer ansehen. Erstellen Sie dazu zunächst einen Prozess mit zahlreichen privaten Seiten im Arbeitssatz. Das haben wir bereits in einem früheren Prozess mithilfe von TestLimit getan: C:\Tools\Sysinternals>Testlimit.exe -d 1 -c 1500 Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - www.sysinternals.com Process ID: 13928 Leaking private bytes with touch 1 MB at a time... Leaked 1500 MB of private memory (1500 MB total leaked). Lasterror: 0 The operation completed successfully. Die Option -d veranlasst TestLimit nicht nur, privaten, zugesicherten Speicher zuzuweisen, sondern auch darauf zuzugreifen. Dadurch wird physischer Speicher zugewiesen und mit dem Prozess verknüpft, um einen Bereich von privatem, zugesichertem virtuellem Arbeitsspeicher zu materialisieren. Sofern das System über ausreichend RAM verfügt, sollten sich die gesamten 1500 MB im RAM für den Prozess befinden. Anschließend wartet der Prozess, bis Sie ihn beenden (etwa dadurch, dass Sie in seinem Befehlsfenster (Strg) + (C) drücken). Gehen Sie danach wie folgt vor: 1. Starten Sie Process Explorer. 2. Öffnen Sie das Menü View, wählen Sie System Information und klicken Sie auf die Registerkarte Memory. 3. Schauen Sie sich die Größen der Listen Free und Zeroed an. 4. Beenden Sie den TestLimit-Prozess. Unter Umständen können Sie erkennen, dass die Liste der freien Seiten kurzzeitig größer wird. Es heißt hier absichtlich »unter Umständen«, da der Nullseitenthread schon aufgeweckt wird, sobald sich acht Seiten auf dieser Liste befinden, und sehr schnell reagiert. Process Explorer aktualisiert seine Anzeige jedoch nur einmal pro Sekunde, weshalb es sehr wahrscheinlich ist, dass die meisten Seiten bereits mit Nullen initialisiert und auf die Nullseitenliste verschoben wurden, bevor der Status erfasst werden kann. Wenn Sie ein vorübergehendes Anwachsen der Liste der freien Seiten erkennen können, werden Sie anschließend einen Abfall auf null und eine entsprechende Vergrößerung der Nullseitenliste sehen. Wenn nicht, so können Sie zumindest ein Anwachsen der Nullseitenliste beobachten. Die PFN-Datenbank Kapitel 5 493
Experiment: Die Standbyliste und die Liste geänderter Seiten Die Verschiebung der Seiten aus dem Arbeitssatz auf die Liste der geänderten Seiten und dann auf die Standbyliste können Sie mit den Sysinternal-Tools VMMap und RAMMap sowie dem Kerneldebugger beobachten. Gehen Sie dazu wie folgt vor: 1. Starten Sie RAMMap und beobachten Sie den Zustand des unbelasteten Systems. Das folgende Beispiel stammt von einem x86-System mit 3 GB RAM. Die Spalten stehen für die einzelnen Seitenstatus aus Abb. 5–37. Der Übersichtlichkeit halber wurden einige für unsere Zwecke unwichtige Spalten ausgeblendet. 2. Das System verfügt über ca. 420 MB freien RAM (die Summe der Nullseitenliste und der Liste freier Seiten). Etwa 580 MB befinden sich auf der Standbyliste (und sind daher »verfügbar«, enthalten aber wahrscheinlich Daten früherer Prozesse oder von SuperFetch). Ungefähr 830 MB sind aktiv, also über gültige PTEs direkt virtuellen Adressen zugeordnet. 3. Jede Zeile schlüsselt die Seitenstatus nach Nutzung oder Ursprung auf (z. B. privater Speicher des Prozesses, zugeordnete Dateien usw.). Beispielsweise sind von den aktiven 830 MB zurzeit 400 MB auf private Prozesszuweisungen zurückzuführen. 4. Erstellen Sie nun wie im vorherigen Experiment mithilfe von TestLimit einen Prozess mit vielen Seiten im Arbeitssatz. Verwenden Sie auch hier wieder die Option -d, damit TestLimit auf jede Seite schreibt, diesmal aber ohne Grenzwert, damit so viele private modifizierte Seiten wie möglich erstellt werden: → 494 Kapitel 5 Speicherverwaltung
C:\Tools\Sysinternals>Testlimit.exe -d Testlimit v5.24 - test Windows limits Copyright (C) 2012-2015 Mark Russinovich Sysinternals - www.sysinternals.com Process ID: 7548 Leaking private bytes with touch (MB)... Leaked 1975 MB of private memory (1975 MB total leaked). Lasterror: 8 5. TestLimit hat jetzt 1975 Zuweisungen von je 1 MB erstellt. Aktualisieren Sie die Anzeige von RAMMap mit File > Refresh. (RAMMap aktualisiert seine Anzeige nicht kontinuierlich, da mit der Erfassung dieser Informationen hohe Kosten verbunden sind.) 6. Wie Sie sehen, sind jetzt mehr als 2,8 GB aktiv, davon 2,4 GB in der Zeile Private Process. Dies ist der von TestLimit zugewiesene und verwendete Speicher. Beachten Sie, dass die Werte für die Standbyliste, die Nullseitenliste und die Liste freier Seiten jetzt viel kleiner sind. Der Großteil des TestLimit zugewiesenen RAM stammt von diesen Listen. → Die PFN-Datenbank Kapitel 5 495
7. Untersuchen Sie nun in RAMMap die Zuweisungen physischer Seiten für den Prozess. Wechseln Sie zur Registerkarte Physical Pages, richten Sie am unteren Bildschirmrand den Filter Process ein und setzen Sie ihn auf Testlimit.exe. Jetzt werden die physischen Seiten angezeigt, die zum Arbeitssatz dieses Prozesses gehören. 8. Wir würden gern eine der physischen Seiten ermitteln, die bei der Zuweisung von virtuellem Adressraum mithilfe der TestLimit-Option -d verwendet werden. RAMMap sagt nichts darüber aus, welche virtuellen Zuweisungen mit den VirtualAlloc-Aufrufen von TestLimit verknüpft sind. Allerdings können wir in VMMap einen guten Hinweis darauf erhalten. Wenn wir VMMap auf denselben Prozess anwenden, erhalten wir folgendes Ergebnis: → 496 Kapitel 5 Speicherverwaltung
9. Im unteren Teil der Anzeige sehen wir Hunderte von Zuweisungen von privaten Prozessdaten von jeweils 1 MB Größe und 1 MB zugesicherter Größe. Das entspricht der Größe der Zuweisungen durch TestLimit. Eine davon ist im vorstehenden Screenshot markiert. Ihre Startadresse lautet 0x310000. 10. Kehren Sie wieder zur Anzeige des physischen Speichers in RAMMap zurück. Ordnen Sie die Spalten so an, dass Sie die Spalte Virtual Address gut erkennen können, klicken Sie darauf, um die Anzeige nach diesem Wert zu sortieren, und suchen Sie die gewünschte virtuelle Adresse: → Die PFN-Datenbank Kapitel 5 497
11. Wie Sie sehen, ist die virtuelle Seite, die bei 0x310000 beginnt, zurzeit der physischen Adresse 0x212D1000 zugeordnet. Die Option -d von TestLimit sorgt dafür, dass in das erste Byte jeder Zuweisung jeweils der Name des Programms geschrieben wird. Das können Sie sich mit dem Befehl !dc (display characters, also Zeichenanzeige, unter Angabe der physischen Adresse) im lokalen Kerneldebugger anschauen: lkd> !dc 0x212d1000 #212d1000 74736554 696d694c 00000074 00000000 TestLimit....... #212d1010 00000000 00000000 00000000 00000000 ................ ... 12. Wenn Sie nicht schnell genug sind, kann das schiefgehen, weil die Seite möglicherweise schon aus dem Arbeitssatz entfernt wurde. Im letzten Abschnitt dieses Experiments wollen wir zeigen, dass diese Daten intakt bleiben (zumindest eine Zeit lang), wenn der Prozessarbeitssatz verkleinert und die Seite auf die Liste der geänderten Seiten und auf die Standbyliste verschoben wird. 13. Markieren Sie in VMMap den Prozess TestLimit, öffnen Sie das Menü View und wählen Sie Empty Working Set, um den Arbeitssatz des Prozesses auf das nackte Minimum zu verkleinern. Daraufhin sehen Sie folgende Anzeige: 14. Das Balkendiagramm Working Set ist praktisch leer. Im mittleren Bereich wird für den Prozess ein Arbeitssatz von lediglich 4 KB angezeigt, und fast alles davon wird für Seitentabellen verwendet. Kehren Sie jetzt zu RAMMap zurück und aktualisieren Sie die Anzeige. Auf der Registerkarte Use Counts sehen Sie jetzt, dass die Anzahl aktiver Seiten erheblich abgenommen hat, während sich viele Seiten auf der Liste der geänderten Seiten und einige auf der Standbyliste befinden: → 498 Kapitel 5 Speicherverwaltung
15. Die Registerkarte Processes von RAMMap bestätigt, dass der TestLimit-Prozess die meisten Seiten zu diesen Listen beigetragen hat: Seitenpriorität Der Speicher-Manager weist jeder physischen Seite im System einen Prioritätswert zu. Dabei handelt es sich um eine Zahl von 0 bis 7. Sie dient vor allem dazu, die Reihenfolge festzulegen, in der die Seiten auf der Standbyliste verwendet werden. Dazu teilt der Speicher-Manager die Standbyliste in acht Teillisten für Seiten der einzelnen Prioritäten auf. Wenn er eine Seite von der Standbyliste entnehmen will, greift er dabei zuerst auf die Liste der niedrigsten Priorität zu. Die PFN-Datenbank Kapitel 5 499
Auch jedem Thread und jedem Prozess im System ist eine Seitenpriorität zugewiesen. Die Priorität einer Seite entspricht gewöhnlich der Seitenpriorität des Threads, der ihre Zuweisung veranlasst hat. (Bei einer gemeinsam genutzten Seite ist dies die höchste Seitenpriorität unter den betreffenden Threads.) Ein Thread erbt seine Seitenpriorität von dem Prozess, zu dem er gehört. Der Speicher-Manager verwendet für Seiten, die er in Erwartung des nächsten Speicherzugriffs eines Prozesses vorausschauend von der Festplatte liest, niedrige Prioritäten. Standardmäßig haben Prozesse einen Seitenprioritätswert von 5. Anwendungen können die Seitenprioritäten von Prozessen und Threads jedoch mit den Benutzermodusfunktionen SetProcessInformation und SetThreadInformation ändern, die wiederum die nativen Funktionen NtSetInformationProcess bzw. NtSetInformationThread aufrufen. Die Speicherpriorität eines Threads können Sie sich in Process Explorer ansehen. (Die Prioritäten der einzelnen Seiten können Sie durch Untersuchung der PFN-Einträge ermitteln, wie Sie in einem Experiment weiter hinten in diesem Kapitel noch sehen werden.) Abb. 5–38 zeigt die Registerkarte Threads von Process Explorer mit Informationen über den Hauptthread von Winlogon. Die Threadpriorität ist zwar hoch, doch die Speicherpriorität hat nach wie vor den Standardwert 5. Abbildung 5–38 Die Registerkarte Threads in Process Explorer Was Speicherprioritäten wirklich leisten, wird erst klar, wenn die relativen Prioritäten der Seiten auf einer höheren Ebene gedeutet werden. Das ist die Rolle von SuperFetch, womit wir uns am Ende dieses Kapitels beschäftigen werden. 500 Kapitel 5 Speicherverwaltung
Experiment: Nach Prioritäten geordnete Standbylisten einsehen Mit Process Explorer können Sie sich die Größen der einzelnen Standbylisten ansehen, indem Sie im Dialogfeld System Information die Registerkarte Memory aufrufen: Auf dem in diesem Experiment verwendeten, erst kürzlich gestarteten x86-System sind ungefähr 9 MB Daten mit Priorität 0 zwischengespeichert, etwa 47 MB mit Priorität 1, 68 MB mit Priorität 2 usw. Versuchen Sie jetzt, mit dem Sysinternals-Tool TestLimit so viel Speicher wie möglich zuzusichern und darauf zuzugreifen: C:\Tools\Sysinternals>Testlimit.exe -d Die Standbylisten niedriger Priorität wurden zuerst verwendet und sind jetzt viel kleiner, während die Listen hoher Priorität nach wie vor wertvolle zwischengespeicherte Daten enthalten. Die PFN-Datenbank Kapitel 5 501
Die Schreibthreads für geänderte und für zugeordnete Seiten Der Speicher-Manager verwendet zwei Systemthreads, um Seiten auf die Festplatte zurückzuschreiben und auf die Standbyliste für ihre Priorität zu setzen. Der eine dieser Threads (MiModifiedPageWriter) schreibt geänderte Seiten in die Auslagerungsdatei, der andere (­MiMappedPageWriter) in zugeordnete Dateien. Zwei Threads sind erforderlich, um Deadlocks zu vermeiden, die auftreten können, wenn beim Schreiben in eine zugeordnete Datei ein Seitenfehler auftritt, weshalb eine freie Seite benötigt wird und, falls keine vorhanden ist, der Schreibthread für geänderte Seiten für weitere freie Seiten sorgen muss. Dadurch, dass der Schreibthread für geänderte Seiten die E/A das Schreiben in zugeordneten Dateien einem zweiten Systemthread überlässt, kann dieser zweite Thread warten, ohne das Schreiben in die Auslagerungsdatei zu blockieren. Beide Threads laufen mit Priorität 18. Nach der Initialisierung warten sie jeweils auf unterschiedliche Ereignisobjekte, von denen sie ausgelöst werden. Der Schreibthread für zugeordnete Seiten wartet auf die folgenden 18 Ereignisobjekte: QQ Ein Beendigungsereignis, das dem Thread die Beendigung signalisiert (für die vorliegende Erörterung ohne Bedeutung). QQ Das MappedPagedWriter-Ereignis, das in der globalen Variablen MiSystemPartition. Modwriter.­MappedPageWriterEvent gespeichert ist (bzw. in MmMappedPageWriterEvent in Windows 8.x und Windows Server 2012/R2). Dieses Ereignis wird in den folgenden Fällen ausgelöst: • Bei einer Seitenlistenoperation durch MiInsertPageInList. Diese Routine fügt eine Seite in die Listen ein, die in seinen Eingangsargumenten angegeben ist. Sie löst das Ereignis aus, wenn die Anzahl der für das Dateisystem bestimmten Seiten in der Liste der geänderten Seiten den Wert 16 überschreitet und die Anzahl der verfügbaren Seiten unter 1024 gefallen ist. • Bei dem Versuch, freie Seiten zu gewinnen (MiObtainFreePages). • Durch den Arbeitssatz-Manager des Speicher-Managers (MmWorkingSetManager). Er löst dieses Ereignis aus, wenn die Anzahl der für das Dateisystem bestimmten Seiten in der Liste der geänderten Seiten den Wert 800 überschreitet. • Bei der Anforderung, alle geänderten Seiten auf die Festplatte zu leeren (MmFlushAllPages). • Bei der Anforderung, alle für das Dateisystem bestimmten geänderten Seiten auf die Festplatte zu leeren (MmFlushAllFilesystemPages). In den meisten Fällen werden geänderte zugeordnete Seiten nicht wieder in die Dateien ihres Hintergrundspeichers zurückgeschrieben, wenn die Anzahl der zugeordneten Seiten in der Liste der geänderten Seiten geringer ist als die Maximalgröße des Schreibclusters, also 16 Seiten. Bei MmFlushAllFilesystemPages und MmFlushAllPages wird diese Überprüfung jedoch nicht durchgeführt. QQ 16 Ereignisse, die mit 16 Listen für zugeordnete Seiten verknüpft und als Array in M­ iSystemPartition.PageLists.MappedPageListHeadEvent gespeichert sind (bzw. in MmMappedPageListHeadEvent in Windows 8.x und Windows Server 2012/R2). Bei jeder Än- 502 Kapitel 5 Speicherverwaltung
derung an einer zugeordneten Seite wird sie in eine dieser 16 Listen aufgenommen. Die Auswahl der Liste erfolgt dabei aufgrund einer Bucketnummer, die in MiSystemPartition. WorkingSetControl->CurrentMappedPageBucket gespeichert ist (bzw. in MmCurrentMappedPageBucket in Windows 8.x und Windows Server 2012/R2). Diese Nummer wird vom Arbeitssatz-Manager immer dann aktualisiert, wenn das System feststellt, dass zugeordnete Seiten alt genug sind. Der Altersschwellenwert beträgt zurzeit 100 Sekunden. Er wird in der Variablen WriteGapCounter in derselben Struktur gespeichert (bzw. in MiWriteGapCounter in Windows 8.x und Windows Server 2012/R2) und bei jeder Ausführung des Arbeitssatz-Managers inkrementiert. Diese zusätzlichen Ereignisse sollen Datenverluste bei einem System­absturz oder Stromausfall verringern helfen, indem geänderte zugeordnete Seiten auch dann geschrieben werden, wenn die Liste der geänderten Seiten den Schwellenwert von 800 Einträgen noch nicht erreicht hat. Der Schreibthread für geänderte Seiten wartet auf zwei Ereignisse. Das erste ist das Beendigungsereignis, und bei dem zweiten handelt es sich um das Ereignis, das in MiSystemPartition. Modwriter.ModifiedPageWriterEvent gespeichert ist (bzw. um das in MmModifiedPageWriterGate gespeicherte Kernelgateereignis in Windows 8.x und Windows Server 2012/R2) und unter folgenden Umständen ausgelöst wird: QQ Bei der Anforderung, alle empfangenen Seiten auf die Festplatte zu leeren. QQ Wenn die Anzahl der verfügbaren Seiten – gespeichert in MiSystemPartition.Vp.AvailablePages (bzw. in MmAvailablePages in Windows 8.x und Windows Server 2012/R2) – unter 128 fällt. QQ Wenn der Gesamtumfang der Nullseitenliste und der Liste freier Seiten auf unter 20.000 fällt und die Anzahl der für die Auslagerungsdatei bestimmten geänderten Seiten größer ist als der kleinere Wert von entweder 1/16 der verfügbaren Seiten oder 64 MB (16.384 Seiten). QQ Wenn ein Arbeitssatz verkleinert wird, um zusätzliche Seiten aufnehmen zu können, und die Anzahl der verfügbaren Seiten kleiner als 15.000 ist. QQ Bei einer Seitenlistenoperation durch MiInsertPageInList. Diese Routine löst das Ereignis aus, wenn die Anzahl der für das Dateisystem bestimmten Seiten in der Liste der geänderten Seiten den Wert 16 überschreitet und die Anzahl der verfügbaren Seiten unter 1024 gefallen ist. Nach dem Auslösen dieses Ereignisses wartet der Schreibthread für geänderte Seiten noch auf zwei weitere Ereignisse. Das eine zeigt an, dass eine erneute Untersuchung der Auslagerungsdatei erforderlich ist (etwa wenn eine neue Auslagerungsdatei erstellt wurde) und ist in MiSystemPartition.Modwriter.RescanPageFilesEvent gespeichert (bzw. in MiRescanPageFilesEvent in Windows 8.x und Windows Server 2012/R2). Das zweite ist ein internes Ereignis des Auslagerungsdateiheaders (MiSystemPartition.Modwriter.PagingFileHeader bzw. MmPagingFileHeader in Windows 8.x und Windows Server 2012/R2) und ermöglicht dem System, das Leeren der Daten in die Auslagerungsdatei bei Bedarf manuell anzufordern. Wenn der Schreibthread für zugeordnete Seiten aufgerufen wird, versucht er, so viele Seiten wie möglich in einer einzigen E/A-Anforderung auf die Festplatte zu schreiben. Dazu untersucht er das ursprüngliche PTE-Feld des PFN-Datenbankelements auf Seiten, die auf der Die PFN-Datenbank Kapitel 5 503
Liste der geänderten Seiten stehen, um Seiten in zusammenhängenden Speicherorten auf der Festplatte zu finden. Wenn eine Liste erstellt ist, werden die Seiten von der Liste der geänderten Seiten entfernt und eine E/A-Anforderung ausgegeben. Nach deren erfolgreichem Abschluss werden die Seiten am Ende der Standbyliste für ihre Priorität eingereiht. Andere Threads können auf Seiten zugreifen, die gerade geschrieben werden. Wenn das geschieht, werden der Referenzzähler und der Zähler für die gemeinsame Nutzung im PFN-Eintrag für die physische Seite heraufgesetzt, um anzugeben, dass ein weiterer Prozess die Seite nutzt. Ist die E/A-Operation abgeschlossen, stellt der Schreibthread für geänderte Seiten fest, dass der Referenzzähler nicht mehr 0 beträgt, weshalb er die Seite nicht auf eine der Standbylisten stellt. PFN-Datenstrukturen PFN-Datenbankeinträge haben zwar eine feste Länge, allerdings können ihre einzelnen Felder je nach dem Seitenstatus eine unterschiedliche Bedeutung aufweisen. Abb. 5–39 zeigt den Aufbau von PFN-Einträgen für verschiedene Zustände. Arbeitssatzindex PTE-Adresse | Sperre Zähler für gemeinsame Nutzung Priorität Flags Typ Cacheattribute Referenzzähler Ursprünglicher PTE-Inhalt PFN des PTEs Flags Seitenfarbe Vorwärtsverknüpfung PTE-Adresse | Sperre Rückwärtsverknüpfung Flags Typ Priorität Cacheattribute Referenzzähler Ursprünglicher PTE-Inhalt PFN des PTEs Flags Seitenfarbe PFN für eine Seite im Arbeitssatz PFN für eine Seite auf der Standby­ liste oder der Liste geänderter Seiten Verknüpfung zur PFN des nächsten Stacks PTE-Adresse | Sperre Zähler für gemeinsame Nutzung Priorität Typ Flags Cacheattribute Referenzzähler Ursprünglicher PTE-Inhalt PFN des PTEs Flags Seitenfarbe Ereignisadresse PTE-Adresse | Sperre Zähler für gemeinsame Nutzung Priorität Typ Flags Cacheattribute Referenzzähler Ursprünglicher PTE-Inhalt PFN des PTEs Flags Seitenfarbe PFN für die Seite eines Kernelstacks PFN für eine Seite mit laufender E/A Kernelstackbesitzer Abbildung 5–39 PFN-Datenbankeinträge für verschiedene Status (prinzipieller Aufbau) Viele der Felder sind bei den verschiedenen PFN-Typen identisch, andere aber haben je nach Art der PFN eine eigene Bedeutung. Die folgenden Felder kommen in mehr als einem PFN-Typ vor: QQ PTE-Adresse Dies ist die virtuelle Adresse des PTEs, der auf die Seite zeigt. Da PTE-Adressen immer an einer 4-Byte-Grenze ausgerichtet sind (bzw. 8 Byte auf 64-Bit-Systemen), werden die beiden unteren Bits als Sperrmechanismus verwendet, um den Zugriff auf den PFN-Eintrag zu serialisieren. 504 Kapitel 5 Speicherverwaltung
QQ Referenzzähler Dies ist die Anzahl der Verweise auf die Seite. Der Zähler wird erhöht, wenn eine Seite zum Arbeitssatz hinzugefügt oder für die E/A im Speicher verriegelt wird (z. B. durch einen Gerätetreiber), und erniedrigt, wenn der Zähler für die gemeinsame Nutzung auf 0 sinkt oder wenn Seiten im Speicher entriegelt werden. In ersterem Fall gehört die Seite keinem Arbeitssatz mehr an. Wenn der Referenzzähler nun auch auf 0 sinkt, wird der PFN-Datenbankeintrag für die Seite aktualisiert, um die Seite der Standliste, der Liste der freien oder der geänderten Seiten hinzuzufügen. QQ Typ Dies ist die Art der Seite, für die die PFN steht (aktiv/gültig, Standby, geändert, geändert und nicht schreibbar, frei, mit null initialisiert, unzulässig oder Übergang). QQ Flags Die möglichen Inhalte des Flagfeldes finden Sie in Tabelle 5–20. QQ Priorität Dies ist die mit dieser PFN verknüpfte Priorität, über die bestimmt wird, in welche Standbyliste die Seite gestellt wird. QQ Ursprünglicher PTE-Inhalt Alle PFN-Datenbankeinträge enthalten den ursprünglichen Inhalt des PTEs, der auf die Seite zeigt (wobei es sich dabei auch um einen Prototyp-PTE handeln kann). Dadurch kann der PTE-Inhalt wiederhergestellt wird, wenn die physische Seite nicht mehr vorhanden ist. Eine Ausnahme bilden PFN-Einträge für AWE-Zuweisungen. Bei ihnen ist in diesem Feld der AWE-Referenzzähler untergebracht. QQ PFN des PTEs Dies ist die Nummer der physischen Seite für die Seitentabelle mit dem PTE, der auf die vorliegende Seite zeigt. QQ Farbe PFN-Datenbankeinträge sind nicht nur über eine Liste verknüpft, sondern enthalten auch ein zusätzliches Feld, um physische Seiten anhand ihrer »Farbe« zu verknüpfen. Dabei handelt es sich um die Nummer des NUMA-Knotens der Seite. QQ Flags Das zweite Flagfeld dient zur Aufnahme zusätzliche Informationen über den PTE. Diese Flags werden in Tabelle 5–21 erklärt. Flag Bedeutung Laufender Schreibvorgang (Write in progress) Gibt an, dass gerade eine Schreiboperation ausgeführt wird. Das erste DWORD enthält die ­Adresse des Ereignisobjekts, das beim Abschluss der E/A ausgelöst wird. Status »Geändert« (Modified state) Gibt an, dass die Seite geändert wurde. Bevor die Seite aus dem Arbeitsspeicher entfernt wird, muss ihr Inhalt auf der Festplatte gesichert werden. Laufender Lesevorgang (Read in progress) Gibt an, dass gerade eine Leseoperation für die Seite ausgeführt wird. Das erste DWORD enthält die Adresse des Ereignisobjekts, das beim Abschluss der E/A ausgelöst wird. ROM Gibt an, dass die Seite aus der Firmware des Computers oder einem anderen schreibgeschützten Speicher stammt, z. B. einem Geräteregister. Einlagerungsfehler (In-page error) Gibt an, dass bei einer Einlagerungsoperation für diese Seite ein E/A-Fehler aufgetreten ist. In diesem Fall enthält das erste Feld im PFN-Eintrag den Fehlercode. → Die PFN-Datenbank Kapitel 5 505
Flag Bedeutung Kernelstack Gibt an, dass die Seite für einen Kernelstack verwendet wurde. In diesem Fall enthält der PFN-Eintrag den Besitzer des Stacks und die PFN des nächsten Stacks für diesen Thread. Entfernen angefordert (Removal requested) Gibt an, dass die Seite das Ziel eines Entfernungsvorgangs ist (aufgrund eines ECC- oder Datenbereinigungsvorgangs oder eines Speicherausbaus im laufenden Betrieb). Paritätsfehler (Parity error) Gibt an, dass die physische Seite Paritäts- oder ECC-Fehler (Error Correction Control) enthält. Tabelle 5–20 Flags in PFN-Datenbankeinträgen Flag Bedeutung PFN-Abbild überprüft (PFN image verified) Gibt an, dass die Codesignatur für die PFN verifiziert wurde. (Diese Signatur ist im Katalog der kryptografischen Signaturen für das Abbild zu dieser PFN enthalten.) AWE-Zuweisung (AWE allocation) Gibt an, dass diese PFN für eine AWE-Zuweisung gilt. Prototyp-PTE Gibt an, dass der PTE, auf den der PFN-Eintrag verweist, ein Prototyp-PTE ist, z. B. bei einer gemeinsam genutzten Seite. Tabelle 5–21 Sekundäre Flags in PFN-Datenbankeinträgen Die restlichen Felder treten nur jeweils in bestimmten Arten von PFN-Einträgen auf. Beispielsweise steht der erste Eintrag in Abb. 5–39 für eine aktive Seite, die Teil eines Arbeitssatzes ist. Hier gibt es ein Feld mit dem Zähler der gemeinsamen Nutzung, der angibt, wie viele PTEs auf die Seite verweisen. (Seiten, die als schreibgeschützt, für Kopieren beim Schreiben oder für gemeinsames Lesen und Schreiben gekennzeichnet sind, können von mehreren Prozessen gemeinsam genutzt werden.) Für die Seiten von Seitentabellen steht in dem entsprechenden Feld dagegen die Anzahl der gültigen und Übergangs-PTEs in der Seitentabelle. Solange der Zähler für die gemeinsame Nutzung größer als 0 ist, kann die Seite nicht aus dem Speicher entfernt werden. Der Arbeitssatzindex gibt den Index in der Prozessarbeitssatzliste an (bzw. in der System- oder Sitzungsarbeitssatzliste; gehört die Seite nicht zu einem Arbeitssatz, ist der Wert 0), an der sich die virtuelle Adresse zur Zuordnung dieser physischen Seite befindet. Bei einer privaten Seite verweist das Feld mit dem Arbeitssatzindex direkt auf den Eintrag in der Arbeitssatzliste, da die Seite nur einer einzigen virtuellen Adresse zugeordnet ist. Bei einer gemeinsam genutzten Seite dagegen ist der Arbeitssatzindex ein Hinweis, dessen Richtigkeit für den ersten Prozess, der die Seite gültig macht, garantiert wird (wobei andere Prozesse nach Möglichkeit versuchen, denselben Index zu verwenden). Der Prozess, der das Feld ursprünglich festgelegt hat, verweist garantiert auf den richtigen Index und muss daher in seinen Arbeitssatz-Hashbaum keinen Hasheintrag für die Arbeitssatzliste aufnehmen, auf den die 506 Kapitel 5 Speicherverwaltung
virtuelle Adresse verweist. Dank dieser Garantie kann der Arbeitssatz-Hashbaum verkleinert werden, was Suchvorgänge für diese Einträge beschleunigt. Der zweite PFN-Eintrag in Abb. 5–39 gilt für eine Seite auf der Standbyliste oder der Liste geänderter Seiten. Die Felder für die Vorwärts- und Rückwärtsverknüpfung verbinden die Listenelemente miteinander. Für Seiten auf diesen Listen ist der Zähler für die gemeinsame Nutzung definitionsgemäß 0 (da kein Arbeitssatz die Seite verwendet) und kann daher durch die Rückwärtsverknüpfung überschrieben werden. Auch der Referenzzähler ist für Seiten auf diesen Listen 0. Ist er nicht 0 (da eine E/A für diese Seite im Gange sein kann, etwa wenn sie auf die Festplatte geschrieben wird), so wird die Seite von der Liste genommen. Der dritte PFN-Eintrag in Abb. 5–39 gilt für eine Seite des Kernelstacks. Wie bereits erwähnt, werden Kernelstacks in Windows dynamisch zugewiesen, erweitert und eingefroren, wenn ein Callback in den Benutzermodus erfolgt oder die Steuerung zurückgibt oder wenn ein Treiber einen Callback durchführt und eine Stackerweiterung anfordert. Für diese PFNs muss sich der Speicher-Manager den Thread merken, der tatsächlich mit dem Kernelstack verknüpft ist, oder eine Verknüpfung zum nächsten freien Look-Aside-Stack unterhalten, wenn die Seite frei ist. Die vierte PFN in Abb. 5–39 gilt für eine Seite, für die gerade ein E/A-Vorgang erfolgt (z. B. ein Lesevorgang). Dabei zeigt das erste Feld auf das Ereignisobjekt, das ausgelöst wird, wenn der E/A-Vorgang abgeschlossen ist. Tritt ein Einlagerungsfehler auf, enthält das Feld den entsprechenden Windows-Fehlerstatuscode. Dieser PFN-Typ dient zur Auflösung von Seitenfehlerkollisionen. Neben der PFN-Datenbank beschreiben auch die in Tabelle 5–22 genannten Systemvariablen den Gesamtstatus des physischen Speichers. Variable (Windows 10 und Windows Server 2016) Variable (Windows 8.x und Windows Server 2012/R2) Beschreibung MiSystemPartition.Vp. NumberOfPhysicalPages MmNumberOfPhysicalPages Gesamtzahl der auf dem System verfügbaren physischen Seiten. MiSystemPartition.Vp. AvailablePages MmAvailablePages Gesamtzahl der auf dem System verfügbaren Seiten – die Summe der Seiten auf der Nullseiten- und der Standbyliste sowie der Liste der freien Seiten. MiSystemPartition.Vp. ResidentAvailablePages MmResidentAvailablePages Gesamtzahl der physischen Seiten, die auf dem System verfügbar wären, wenn die Arbeitssätze aller Prozesse auf ihre Mindestgröße geschrumpft und alle geänderten Seiten auf die Festplatte geleert würden. Tabelle 5–22 Systemvariablen zur Beschreibung des physischen Speichers Die PFN-Datenbank Kapitel 5 507
Experiment: PFN-Einträge einsehen Mit dem Kerneldebuggerbefehl !pfn können Sie einzelne PFN-Einträge untersuchen. Dabei müssen Sie die PFN als Argument angeben, beispielsweise !pfn 0 für den ersten Eintrag, !pfn 1 für den zweiten usw. Die folgende Ausgabe zeigt den PTE für die virtuelle Adresse 0xD20000, gefolgt von dem PFN-Eintrag mit dem Seitenverzeichnis und dann der eigentlichen Seite: lkd> !pte d20000 VA 00d20000 PDE at C0600030 PTE at C0006900 contains 000000003E989867 contains 8000000093257847 pfn 3e989 ---DA--UWEV pfn 93257 ---D---UW-V lkd> !pfn 3e989 PFN 0003E989 at address 868D8AFC flink blink / share count 00000144 pteaddress C0600030 reference count 0001 00000071 Cached color Priority 5 restore pte 00000080 containing page Active M 0 0696B3 Modified lkd> !pfn 93257 PFN 00093257 at address 87218184 flink blink / share count 00000001 pteaddress C0006900 reference count 0001 000003F9 Cached color Priority 5 restore pte 00000080 containing page Active M 0 03E989 Modified Auch mit MemInfo können Sie Informationen über eine PFN abrufen. Dabei erhalten Sie manchmal sogar noch mehr Angaben als in der Ausgabe des Debuggers, und überdies ist es dabei nicht nötig, im Debuggingmodus zu starten. Die MemInfo-Ausgabe für dieselben beiden PFNs sieht wie folgt aus: C:\Tools>MemInfo.exe -p 3e989 0x3E989000 Active Page Table 5 N/A 0xC0006000 0x8E499480 C:\Tools>MemInfo.exe -p 93257 0x93257000 Active Process Private 5 windbg.exe 0x00D20000 N/A Von links nach rechts sehen Sie hier die physische Adresse, den Typ, die Seitenpriorität, den Prozessnamen, die virtuelle Adresse sowie mögliche weitere Informationen. MemInfo hat korrekt erkannt, dass es sich bei der ersten PFN um eine Seitentabelle handelt und dass die zweite zu WinDbg gehört, dem aktiven Prozess, als der Befehl !pte d20000 im Debugger verwendet wurde. 508 Kapitel 5 Speicherverwaltung
Reservierungen in der Auslagerungsdatei Sie haben bereits einige Mechanismen kennengelernt, mit denen der Speicher-Manager versucht, den Verbrauch an physischem Speicher und damit den Zugriff auf Auslagerungsdateien zu verringern. Dazu gehören die Verwendung der Standbyliste und der Liste geänderter Seiten sowie die Speicherkomprimierung, die in einem eigenen Abschnitt weiter hinten in diesem Kapitel erklärt wird. Ein weiteres Optimierungsverfahren steht in direktem Zusammenhang mit dem Zugriff auf die Auslagerungsdateien. Rotationsfestplatten haben einen beweglichen Kopf, der erst zum Zielsektor gefahren werden muss, bevor der eigentliche Lese- oder Schreibvorgang beginnen kann. Diese Suchzeit ist relativ lang (in der Größenordnung von Millisekunden). Die gesamte Zeit für Festplattenaktivitäten setzt sich aus der Suchzeit und der eigentlichen Schreib- oder Lesezeit zusammen. Wenn sehr viele Daten von der erreichten Suchposition aus in einem zusammenhängenden Gebiet zugänglich sind, dann kann die Suchzeit vernachlässigbar werden. Muss der Kopf dagegen viel suchen, weil die Daten über die Festplatte verstreut sind, wird die Gesamtzeit für die Suchvorgänge zum Hauptproblem. Wenn der Sitzungs-Manager (Smss.exe) eine Auslagerungsdatei erstellt, fragt er die Festplatten- oder Dateipartition danach ab, ob er es mit einer Rotations- oder einer SSD-Festplatte zu tun hat (Solid-State Drive). Im ersten Fall aktiviert er den Mechanismus der sogenannten Reservierungen in der Auslagerungsdatei. Dabei wird versucht, zusammenhängende Seiten im physischen Speicher auch in der Auslagerungsdatei zusammenhängend zu halten. Bei einem SSD-Laufwerk (oder einem Hybridmodell, das jedoch, was diese Reservierungen angeht, ebenso behandelt wird) bringen Reservierungen in der Auslagerungsdatei keinen echten Nutzen (da es keinen beweglichen Kopf gibt), weshalb dieser Mechanismus hier nicht genutzt wird. Reservierungen in der Auslagerungsdatei werden an drei Stellen im Speicher-Manager gehandhabt, nämlich im Arbeitssatz-Manager, im Schreibthread für geänderte Seiten und im Seitenfehlerhandler. Der Arbeitssatz-Manager verkleinert Arbeitssätze, indem er die Routine MiFreeWsleList aufruft, die eine Liste von Seiten im Arbeitssatz entgegennimmt und für jede dieser Seiten den Zähler für die gemeinsame Nutzung heruntersetzt. Fällt dieser Zähler auf 0, kann die Seite in die Liste der geänderten Seiten geschoben werden, wobei der zugehörige PTE in einen Übergangs-PTE geändert wird. Der alte gültige PTE wird im PFN-Eintrag gespeichert. Der ungültige PTE enthält zwei Bits für den Reservierungsmechanismus, nämlich für Reservierungen und für Zuweisungen in der Auslagerungsdatei (siehe Abb. 5–24). Wird eine physische Seite benötigt und wird sie aus einer der Listen für »freie« Seiten entnommen (Liste freier Seiten, Nullseiten- oder Standbyliste) und zu einer aktiven (gültigen) Seite gemacht, wird im PFN-Feld für den ursprünglichen PTE ein ungültiger PTE gespeichert. Dieses Feld ist von entscheidender Bedeutung, um den Überblick über Reservierungen in der Auslagerungsdatei zu behalten. Die Routine MiCheckReservePageFileSpace versucht, einen Reservierungscluster zu erstellen, bei der die angegebene Seite beginnt. Sie prüft, ob Reservierungen für die vorgesehene Auslagerungsdatei deaktiviert sind und ob es bereits eine solche Reservierung für die Datei gibt (anhand des ursprünglichen PTEs). Wenn eine dieser Bedingungen wahr ist, bricht die Funktion die weitere Verarbeitung der Seite ab. Des Weiteren prüft die Routine, ob es sich um Benutzerseiten handelt. Ist das nicht der Fall, bricht sie ebenfalls ab. Für andere Arten von Seiten (z. B. für Die PFN-Datenbank Kapitel 5 509
den ausgelagerten Pool) werden keine Reservierungen versucht, da sie dafür nicht besonders nützlich sind (u. a. aufgrund der nicht vorhersehbaren Nutzungsmuster, die zu kleinen Clustern führen). Schließlich ruft MiCheckReservePageFileSpace die Funktion MiReservePageFileSpace auf, um die eigentliche Arbeit zu erledigen. Die Suche nach Reservierungen in der Auslagerungsdatei erfolgt abwärts und beginnt beim ursprünglichen PTE. Das Ziel besteht darin, geeignete, aufeinanderfolgende Seiten zu finden, die reserviert werden können. Wenn der PTE, der die benachbarte Seite zuordnet, zu einer nicht zugesicherten Seite, zu einer Seite aus dem nicht ausgelagerten Pool oder zu einer bereits reservierten Seite gehört, kann die Seite nicht genutzt werden. In diesem Fall wird die aktuelle Seite zur unteren Grenze des Reservierungsclusters. Anderenfalls geht die Suche nach unten weiter. Anschließend beginnt die Suche erneut von der ursprünglichen Seite aus vorwärts, um auch dort so viele geeignete Seiten wie möglich zu erfassen. Der Cluster muss mindestens 16 Seiten umfassen, damit die Reservierung stattfindet (wobei die maximale Clustergröße 512 Seiten beträgt). Abb. 5–40 zeigt ein Beispiel für einen Cluster, der auf der einen Seite durch eine ungültige Seite und auf der anderen durch einen vorhandenen Cluster begrenzt ist. (Ein Cluster kann auch mehrere Seitentabellen überspannen, sofern sie zum selben Seitenverzeichnis gehören.) Ein weiterer Cluster für die Reservierung in der Auslagerungsdatei Start-PTE Cluster für die Reservierung in der Auslagerungsdatei (Der Start-PTE befindet sich im Übergangszustand.) UNGÜLTIG UNGÜLTIG UNGÜLTIG Seitenverzeichnis für Prozess A Nicht zugewiesene Region Seitentabellen für Prozess A Abbildung 5–40 Cluster für Reservierungen in der Auslagerungsdatei Nach der Berechnung des Reservierungsclusters muss freier Platz in der Auslagerungsdatei gefunden werden, um ihn für diesen Cluster zu reservieren. Zuweisungen in der Auslagerungsdatei werden mithilfe einer Bitmap gehandhabt, wobei jedes gesetzte Bit für eine verwendete Seite in der Datei steht. Für Reservierungen in der Auslagerungsdatei wird eine zweite Bitmap verwendet, bei der die gekennzeichneten Seiten reserviert wurden. Das heißt jedoch nicht unbedingt, dass sie auch tatsächlich geschrieben wurden, denn das ist die Aufgabe der Bitmap für die Zuweisungen in der Auslagerungsdatei. Wenn es in der Auslagerungsdatei Platz gibt, der gemäß diesen Bitmaps weder reserviert noch zugewiesen ist, werden die zugehörigen Bits in 510 Kapitel 5 Speicherverwaltung
der Reservierungsbitmap gesetzt. Wenn der Schreibthread für geänderte Seiten den Inhalt der Seiten auf die Festplatte schreibt, muss er die entsprechenden Bits in der Zuweisungsbitmap setzen. Kann in der Auslagerungsdatei kein ausreichender Platz für den vorgesehenen Cluster gefunden werden, wird versucht, die Datei zu erweitern. Falls das bereits geschehen ist (oder falls die Auslagerungsdatei bereits die maximale Größe erreicht hat), wird der Cluster verkleinert, damit er in den vorhandenen Platz für eine Reservierung passt. Die Seiten im Cluster (mit Ausnahme des ursprünglichen Start-PTEs) sind nicht mit irgendeiner der Listen physischer Seiten verknüpft. Die Reservierungsinformationen werden in den ursprünglichen PTE der PFN aufgenommen. Der Schreibthread für geänderte Seiten muss das Schreiben von Seiten mit Reservierungen als Sonderfall behandeln. Er nutzt alle zuvor beschriebenen Informationen, um eine MDL mit den korrekten PFNs für einen umfassenden Cluster aufzustellen, den er zum Schreiben verwendet. Für den Aufbau dieses Schreibclusters muss er zusammenhängende Seiten finden, die alle Reservierungscluster überspannen. Bei »Lücken« zwischen den Einzelclustern wird eine Platzhalterseite eingeschaltet (in der alle Bytes den Wert 0xFF haben). Bei mehr als 32 Platzhalterseiten ist der Schreibcluster zersplittert. Dieser Durchlauf erfolgt erst vorwärts und dann rückwärts, um den endgültigen Schreibcluster zu erstellen. Abb. 5–41 zeigt ein Beispiel für den Seitenstatus, nachdem der Schreibthread für geänderte Seiten einen solchen Schreibcluster erstellt hat. Ungültig Aktiv Ungültig Schutz Schutz Ursprünglicher PTE Ursprünglicher PTE PTE-Zeiger PTE-Zeiger PFN 1F6 PFN 67A Aktiv Ungültig Aktiv Seitentabellen von Prozess A Aktiv Schutz Ungültig Ursprünglicher PTE Aktiv PTE-Zeiger Seitentabellen von Prozess B PFN 89C Reservierung 1 in der Auslagerungsdatei für Prozess A PLATZHALTER PLATZHALTER PFN 1F6 PLATZHALTER PFN 53A PLATZHALTER Reservierung 1 in der Auslagerungsdatei für Prozess B PFN 67A PLATZHALTER PFN 89C PLATZHALTER PFN 10F PLATZHALTER PLATZHALTER Zuweisungsbitmap Reservierungsbitmap Systemauslagerungsdatei Abbildung 5–41 Der Schreibcluster Schließlich bestimmt der Seitenfehlerhandler anhand der Informationen in der Reservierungsbitmap und in den PTEs die Anfangs- und Endpunkte der Cluster, um benötigte Seiten mit minimalem Suchaufwand des mechanischen Kopfes der Festplatte zurückladen zu können. Die PFN-Datenbank Kapitel 5 511
Grenzwerte für den physischen Speicher Nachdem Sie nun wissen, wie Windows den Überblick über den physischen Speicher behält, sehen wir uns als Nächstes an, mit wie viel Speicher Windows tatsächlich umgehen kann. Da die meisten Systeme auf mehr Code und Daten zugreifen, als in den physischen Speicher passt, ist dieser physische Speicher im Grunde genommen ein Fenster, das einen Ausschnitt des im Laufe der Zeit verwendeten Codes und der Daten zeigt. Die Menge des Speichers beeinflusst die Leistung, denn wenn Daten oder Code von einem Prozess oder dem Betriebssystem benötigt werden, aber nicht vorhanden sind, muss der Speicher-Manager sie erst von der Festplatte oder einem Remotespeicher beziehen. Doch die Menge des physischen Speichers wirkt sich nicht nur auf die Leistung, sondern auch auf die Verfügbarkeit mancher Ressourcen aus. Beispielsweise ist der Umfang des nicht ausgelagerten Pools durch den Umfang des physischen Speichers beschränkt. Des Weiteren ist der physische Speicher auch einer der bestimmenden Faktoren für die Höchstgröße des virtuellen Speichers, die sich grob ausgedrückt aus der Größe des physischen Speichers zuzüglich der aktuellen Größe aller Auslagerungsdateien zusammensetzt. Der physische Speicher wirkt sich indirekt auch auf die Höchstzahl der möglichen Prozesse aus. Wie viel physischen Speicher Windows handhaben kann, hängt von Hardwareeinschränkungen, der Lizenzierung, den Datenstrukturen des Betriebssystems und der Treiberkompatibilität ab. Auf https://msdn.microsoft.com/en-us/library/windows/desktop/aa366778.aspx finden Sie die Speichergrenzwerte für verschiedene Windows-Editionen. Tabelle 5–23 gibt einen Überblick über die Grenzwerte für Windows 8 und höher. Version/Edition Maximum (32 Bit) Maximum (64 Bit) Windows 8.x Professional und Enterprise 4 GB 512 Windows 8.x (alle anderen Editionen) 4 GB 128 GB Windows Server 2012/R2 Standard und Datacenter – 4 TB Windows Server 2012/R2 Essentials – 64 GB Windows Server 2012/R2 Foundation – 32 GB Windows Storage Server 2012 Workgroup – 32 GB Windows Storage Server 2012 Standard Hyper-V Server 2012 – 4 TB Windows 10 Home 4 GB 128 GB Windows 10 Pro, Education und Enterprise 4 GB 2 TB Windows Server 2016 Standard und Datacenter – 24 TB Tabelle 5–23 Grenzwerte für den physischen Speicher 512 Kapitel 5 Speicherverwaltung
Die maximal unterstützte Menge an Arbeitsspeicher beträgt zurzeit 4 TB auf einigen Editionen von Windows Server 2012/R2 bzw. 24 TB auf Windows Server 2016. Diese Grenzwerte beruhen jedoch nicht auf irgendwelchen Einschränkungen der Implementierung oder der Hardware, sondern rühren daher, dass Microsoft nur Konfigurationen unterstützt, die das Unternehmen auch testen kann. Die genannten Speichermengen sind zurzeit die größten, die überprüft wurden. Speichergrenzwerte für Windows-Clienteditionen Die einzelnen 64-Bit-Clienteditionen von Windows unterscheiden sich in der Menge an unterstütztem physischem Speicher, wobei die Spanne von 4 GB bis zu 2 TB in den Enterprise- und Professional-Editionen reicht. Für alle 32-Bit-Clienteditionen dagegen gilt ein Maximum von 4 GB, was der größte physische Speicher ist, der mit dem Standard-Speicherverwaltungsmodus von x86-Systemen zugänglich ist. Client-SKUs unterstützen den PAE-Adressierungsmodus auf x86, um einen Hardwareschutz zu ermöglichen. Damit wäre es zwar möglich, auf mehr als 4 GB an physischem Speicher zuzugreifen, doch in Tests hat sich gezeigt, dass solche Systeme abstürzen, sich aufhängen oder nicht mehr starten lassen. Das liegt daran, dass einige Gerätetreiber – hauptsächlich diejenigen für Video- und Audiogeräte, die auf Clientcomputern gebräuchlich sind, aber nicht auf Servern – nicht darauf programmiert wurden, physische Adressen größer als 4 GB zu erwarten. Daher schneiden diese Treiber solche Adressen ab, was zu Speicherbeschädigungen und damit verbundenen Nebenwirkungen führt. Serversysteme verfügen dagegen über weniger spezifische Treiber, die einfacher und stabiler sind, weshalb diese Probleme bei ihnen in der Regel nicht auftreten. Aufgrund der problematischen Clienttreiberumgebung wurde die Entscheidung gefällt, dass Clienteditionen den physischen Speicher oberhalb von 4 GB ignorieren, auch wenn sie ihn theoretisch adressieren könnten. Treiberentwicklern wird empfohlen, ihre Systeme mit der BCD-Option nolowmem zu testen, um den Kernel zu zwingen, physische Adressen oberhalb von 4 GB nur dann zu nutzen, wenn auf dem System ausreichend Speicher dazu vorhanden ist. Dadurch können die entsprechenden Probleme in Treibern sofort erkannt werden. Der Betrag von 4 GB stellt den Lizenzierungsgrenzwert für 32-Bit-Clienteditionen dar, doch der tatsächliche Grenzwert kann niedriger sein und hängt vom Chipsatz des Systems und den angeschlossenen Geräten ab. Das liegt daran, dass die Zuordnung der physischen Adressen nicht nur den RAM, sondern auch den Gerätespeicher einschließt, wobei x86- und x64-Systeme gewöhnlich den gesamten Gerätespeicher unterhalb der Grenze von 4 GB zuordnen, um mit 32-Bit-Systemen kompatibel zu bleiben, die nicht wissen, wie sie mit Adressen größer als 4 GB umgehen sollen. Modernere Chipsätze unterstützen zwar eine Geräteneuzuordnung auf der Grundlage von PAE, doch Windows-Clienteditionen machen aufgrund der erwähnten Treiberkompatibilitätsprobleme keinen Gebrauch davon. Die Treiber würden sonst 64-Bit-Zeiger zu ihrem Gerätespeicher erhalten. Wenn Geräte wie Video-, Audio- und Netzwerkadapter auf einem System mit 4 GB RAM Fenster mit Ausschnitten ihres Gerätespeichers von insgesamt 500 MB in den Speicher einfügen, dann landen 500 MB der 4 GB RAM oberhalb der 4-GB-Grenze (siehe Abb. 5–42). Grenzwerte für den physischen Speicher Kapitel 5 513
4,5 GB 4 GB Unzugänglicher RAM RAM Gerätespeicher RAM Gerätespeicher RAM 0 Abbildung 5–42 Aufbau des physischen Speichers auf einem 4-GB-System Bei einer 32-Bit-Clientversion von Windows kann es daher sein, dass Sie auf einem System mit 3 GB oder mehr nicht den gesamten RAM nutzen können. Im Dialogfeld Systemeigenschaften wird angezeigt, wie viel installierten RAM Windows erkannt hat, aber um den tatsächlich für Windows verfügbaren Speicher zu ermitteln, müssen Sie auf der Registerkarte Leistung des Task-Managers nachschauen oder das Hilfsprogramm Msinfo32 verwenden. Beispielsweise zeigt Msinfo32 auf einer Hyper-V-VM mit 4 GB RAM und einer 32-Bit-Version von Windows 10 einen verfügbaren physischen Speicher von 3,87 GB an: Installed Physical Memory (RAM) 4.00 GB Total Physical Memory 3.87 GB Den Aufbau des physischen Speichers können Sie sich mit dem Programm MemInfo ansehen. Die folgende MemInfo-Ausgabe stammt von einem 32-Bit-System. Mit dem Schalter -r haben wir hier die physischen Speicherbereiche ausgegeben: C:\Tools>MemInfo.exe -r MemInfo v3.00 - Show PFN database information Copyright (C) 2007-2016 Alex Ionescu www.alex-ionescu.com Physical Memory Range: 00001000 to 0009F000 (158 pages, 632 KB) Physical Memory Range: 00100000 to 00102000 (2 pages, 8 KB) Physical Memory Range: 00103000 to F7FF0000 (1015533 pages, 4062132 KB) MmHighestPhysicalPage: 1015792 In den Adressbereichen klafft eine Lücke zwischen A0000 und 100000 (384 KB) sowie eine weitere zwischen F8000000 bis FFFFFFFF (128 MB). 514 Kapitel 5 Speicherverwaltung
Mit dem Geräte-Manager können Sie sich ansehen, wer diese reservierten Speicherregionen belegt hat, die Windows nicht nutzen kann und die daher in der Ausgabe von MemInfo als Lücken angezeigt werden. Gehen Sie dazu wie folgt vor: 1. Führen Sie Devmgmt.msc aus. 2. Öffnen Sie das Menü Ansicht und wählen Sie Ressourcen nach Verbindung. 3. Erweitern Sie den Knoten Arbeitsspeicher. Auf dem Laptop, auf dem die Ausgabe aus Abb. 5–43 erstellt wurde, ist der Hauptverbraucher des zugeordneten Gerätespeichers wie zu erwarten die Videokarte (Hyper-V S3 Cap), die 128 MB im Bereich F8000000 bis FBFFFFFF belegt. Arbeitsspeicher [00000000 - 0009FFFF] Systemplatine [000A0000 - 000BFFFF] PCI-Bus [000C0000 - 000DFFFF] Systemplatine [000E0000 - 000FFFFF] Systemplatine [00100000 - F7FFFFFF] Systemplatine [E0000000 - FFFFFFFF] PCI-Bus [FF800000 - FFFFFFFF] Microsoft Hyper-V Video [F8000000 - FFFBFFFF] PCI-Bus [F8000000 - FBFFFFFF] Microsoft Hyper-V S3 Cap [FEBFF000 - FEBFFFFF] Intel 21140-Based PCI Fast Ethernet Adapter (Emuliert) [FEC00000 - FEC00FFF] Hauptplatinenressourcen [FEE00000 - FEE00FFF] Hauptplatinenressourcen [FFFC0000 - FFFFFFFF] Systemplatine Abbildung 5–43 Von der Hardware reservierte Speicherbereiche auf einem 32-Bit-Windows-System Für die restlichen Lücken sind verschiedene weitere Gerätetreiber verantwortlich. Aufgrund der vorsichtigen Schätzungen der Firmware während des Bootvorgangs reserviert der PCI-Bus weitere Bereiche für Geräte. Speicherkomprimierung Der Speicher-Manager von Windows 10 verfügt über einen Mechanismus, um private und auslagerungsgepufferte Seiten zu komprimieren, die sich auf der Liste der geänderten Seiten befinden. Die Hauptkandidaten für diese Komprimierung sind private Seiten von UWP-Apps, da die Komprimierung gut mit der Auslagerung und Leerung des Arbeitssatzes zusammenwirkt, die bei solchen Anwendungen ohnehin schon stattfinden, wenn der Speicher knapp ist. Wenn eine Anwendung angehalten und ihr Arbeitssatz ausgelagert ist, ist es jederzeit möglich, ihren Arbeitssatz zu leeren und geänderte Seiten zu komprimieren. Dadurch wird zusätzlicher Speicher verfügbar, was schon ausreichen kann, um eine weitere Anwendung im Speicher zu halten, ohne dass die Seiten der ersten Anwendung ihn verlassen müssen. Experimente haben gezeigt, dass Seiten durch den Xpress-Algorithmus von Microsoft auf ca. 30–50 % ihrer ursprünglichen Größe komprimiert werden können. Dabei wird die Geschwindigkeit für die Größe geopfert, was zu erheblichen Speichereinsparungen führt. Speicherkomprimierung Kapitel 5 515
Die Speicherkomprimierung muss die folgenden Voraussetzungen erfüllen: QQ Eine Seite darf sich nicht sowohl in komprimierter als auch in nicht komprimierter Form befinden, denn eine solche Duplizierung würde eine Verschwendung von physischem Speicher bedeuten. Wenn eine Seite komprimiert wird, muss die Originalseite anschließend sofort freigegeben werden. QQ Der Komprimierungsspeicher muss die komprimierten Daten so speichern und seine Datenstrukturen so unterhalten, dass sich dabei stets eine Einsparung an Speicher für das System insgesamt ergibt. Das bedeutet, dass eine Seite, die nicht gut komprimiert werden kann, nicht zum Speicher hinzugefügt werden sollte. QQ Komprimierte Seiten müssen als verfügbarer Speicher angezeigt werden, denn schließlich können sie bei Bedarf tatsächlich umgewidmet werden. Das dient dazu, die fehlerhafte Wahrnehmung zu vermeiden, dass die Komprimierung den Speicherverbrauch erhöht. Die Speicherkomprimierung ist auf allen Client-SKUs (Telefon, PC, Xbox usw.) standardmäßig aktiviert. Auf Server-SKUs wird zurzeit keine Speicherkomprimierung verwendet, allerdings wird sich das in künftigen Versionen wahrscheinlich ändern. In Windows Server 2016 zeigt der Task-Manager in Klammern die Werte für komprimierten Speicher an, die aber immer 0 sind. Es gibt auch keinen Speicherkomprimierungsprozess. Beim Systemstart weist der Dienst SuperFetch (Sysmain.dll, ausgeführt in einer Instanz von Svchost.exe; siehe den Abschnitt »Vorausschauende Speicherverwaltung (SuperFetch)« weiter hinten in diesem Kapitel) den Store-Manager in der Exekutive durch einen Aufruf von NtSetSystemInformation an, einen Systemspeicher für Nicht-UWP-Anwendungen zu erstellen. (Dies ist stets der erste Systemspeicher, der angelegt wird.) Wird eine UWP-Anwendung gestartet, fordert sie von SuperFetch einen Speicher für sich selbst an. Ablauf der Komprimierung Um Ihnen eine Vorstellung davon zu geben, wie die Speicherkomprimierung funktioniert, zeigen wir ein Beispiel zur Veranschaulichung an. Nehmen wir an, es gibt die folgenden physischen Seiten: Null/Frei Aktiv Geändert 516 Kapitel 5 Speicherverwaltung
Die Nullseitenliste und die Liste der freien Seiten enthalten Seiten, die mit Abfall bzw. mit Nullen gefüllt sind und zur Zusicherung von Speicher verwendet werden können. Für unsere Zwecke betrachten wir sie hier als eine gemeinsame Liste. Die aktiven Seiten gehören zu verschiedenen Seiten, während die geänderten Seiten modifizierte Daten enthalten, die noch nicht in eine Auslagerungsdatei geschrieben wurden. Wenn ein Prozess auf eine solche Seite verweist, kann sie jedoch über einen weichen Seitenfehler in den Arbeitssatz des Prozesses aufgenommen werden, ohne dass dabei eine E/A-Operation erforderlich wäre. Nehmen wir nun an, dass der Speicher-Manager die Liste der geänderten Seiten verkleinern will, etwa weil sie zu groß geworden ist oder die Null/Frei-Liste zu klein. Gehen wir des Weiteren davon aus, dass aus der Liste der geänderten Seiten drei Seiten entfernt werden sollen. Der Speicher-Manager komprimiert nun den Inhalt zu einer einzigen Seite, die der Null/ Frei-Seite entnommen wird: Null/Frei Aktiv Geändert Die Seiten 11 bis 13 sind nun in Seite 1 komprimiert. Danach ist Seite 1 nicht mehr frei, sondern aktiv, da sie zum Arbeitssatz des Speicherkomprimierungsprozesses gehört (siehe den nächsten Abschnitt). Nun werden die Seiten 11 bis 13 nicht mehr benötigt und können daher in die Liste der freien Seiten verschoben werden. Durch die Komprimierung werden also unter dem Strich zwei Seiten eingespart: Null/Frei Aktiv Aktiv (Speicherkomprimierungsprozess) Geändert Speicherkomprimierung Kapitel 5 517
Stellen Sie sich nun vor, dass der Vorgang ein weiteres Mal ausgeführt wird. Dieses Mal werden die Seiten 14 bis 16 in zwei Seiten (2 und 3) komprimiert: Null/Frei Aktiv Aktiv (Speicherkomprimierungsprozess) Geändert Dadurch gelangen die Seiten 2 und 3 in den Arbeitssatz des Speicherkomprimierungsprozesses, während die Seiten 14 bis 16 frei werden: Null/Frei Aktiv Aktiv (Speicherkomprimierungsprozess) Geändert Wenn der Speicher-Manager später beschließt, den Arbeitssatz des Speicherkomprimierungsprozesses zu verkleinern, werden dessen Seiten in die Liste der geänderten Seiten verschoben, da sie Daten enthalten, die noch nicht in eine Auslagerungsdatei geschrieben wurden. Natürlich können sie jederzeit durch einen weichen Seitenfehler zu ihrem ursprünglichen Prozess zurückkehren, wobei sie unter Verwendung freier Seiten entkomprimiert werden. In der folgenden Abbildung wurden die Seiten 1 und 2 von den aktiven Seiten des Speicherkomprimierungsprozesses entfernt und in die Liste der geänderten Seiten verschoben: 518 Kapitel 5 Speicherverwaltung
Null/Frei Aktiv Aktiv (Speicherkomprimierungsprozess) Geändert Wird der Speicher knapp, kann der Speicher-Manager entscheiden, die komprimierten geänderten Dateien in eine Auslagerungsdatei zu schreiben: Null/Frei Aktiv Aktiv (Speicherkomprimierungsprozess) Geändert Auslagerungs­ datei Danach werden diese Seiten in die Standbyliste verschoben, da ihr Inhalt gespeichert ist und sie bei Bedarf einem anderen Zweck zugeführt werden können. Ebenso wie in der Liste der geänderten Seiten können sie auch jetzt bei einem weichen Seitenfehler entkomprimiert werden, woraufhin die resultierenden Seiten als aktive Seiten dem Arbeitssatz des entsprechenden Prozesses zugeschlagen werden. In der Standbyliste stehen sie jeweils in der Teilliste für ihre Priorität (siehe den Abschnitt »Seitenpriorität und Rebalancing« weiter hinten in diesem Kapitel). Speicherkomprimierung Kapitel 5 519
Null/Frei Aktiv Aktiv (Speicherkomprimierungsprozess) Geändert Standby Komprimierungsarchitektur Die Komprimierungs-Engine benötigt Speicher als »Arbeitsbereich«, um die komprimierten Seiten und die Datenstrukturen zu ihrer Verwaltung festzuhalten. In Windows 10 vor Version 1607 wurde dazu der Benutzeradressraum des Systemprozesses verwendet, ab Version 1607 dagegen der neue, eigens dafür vorgesehene Speicherkomprimierungsprozess. Ein Grund dafür war, dass der Speicherverbrauch des Systemprozesses bei flüchtiger Betrachtung hoch wirkte, was zu der Schlussfolgerung Anlass gab, dass das System viel Speicher verbrauchen würde. Das ist jedoch nicht der Fall, da der komprimierte Speicher nicht auf den Zusicherungsgrenzwert angerechnet wird. Manchmal ist jedoch die Wahrnehmung entscheidend. Der Speicherkomprimierungsprozess ist ein minimaler Prozess, das heißt, er lädt keine DLLs und führt keine Abbilder aus, sondern stellt nur einen Benutzermodus-Adressraum bereit, den der Kernel nutzen kann. (Mehr über minimale Prozesse erfahren Sie in Kapitel 3.) In der Detailansicht des Task-Managers wird der Speicherkomprimierungsprozess absichtlich nicht angezeigt. In Process Explorer ist er jedoch zu sehen. Im Kerneldebugger lautet der Abbildname für diesen Prozess MemCompression. Für jeden einzelnen Speicher (»store«) weist der Store-Manager Arbeitsspeicher (»memory«) in Regionen mit einstellbarer Größe zu, zurzeit 128 KB. Dies geschieht gewöhnlich nach Be­­ darf durch Aufrufe von VirtualAlloc. Die eigentlichen komprimierten Seiten werden in 16Byte-Stücken in einer Region gespeichert. Eine komprimierte Seite (4 KB) kann offensichtlich viele solcher Stücke umfassen. Abb. 5–44 zeigt einen Speicher mit einem Array von Regionen und einigen der Datenstrukturen, die zur Verwaltung erforderlich sind. 520 Kapitel 5 Speicherverwaltung
Regionsmetadaten B+-Baum der Seiten Idx Genutzter Platz Regionsarray Seiteneintrag Region 0 Seitenschlüssel Regions-Nr. Offset Größe Flags Region 0 Nicht verwendet Region 1 Nicht verwendet Region N Regionselement Komprimierte Seiten Regionseintrag Regions-Nr. Offset Virtuelle Adresse Reset-Bitmap Seitenschlüssel B+-Baum der Regionen Region N Abbildung 5–44 Speicherdatenstrukturen Wie Sie in Abb. 5–44 sehen, werden die Seiten mit einem B+-Baum verwaltet. Im Grunde genommen ist das ein Baum, dessen Knoten jeweils eine beliebige Anzahl von Kindknoten aufweisen können. Darin zeigt jeder Seiteneintrag auf den zugehörigen komprimierten Inhalt in einer der Regionen. Zu Anfang hat ein Speicher null Regionen. Die Zuweisung von Regionen und die Aufhebung dieser Zuweisungen erfolgen nach Bedarf. Wie Sie im Abschnitt »Seitenpriorität und Rebalancing« weiter hinten in diesem Kapitel noch sehen werden, hat jede Region eine Priorität. Um eine Seite zu einem Speicher hinzuzufügen, sind die folgenden Hauptschritte erforderlich: 1. Wenn es keine Region mit der Priorität der hinzuzufügenden Seite gibt, wird eine neue Region zugewiesen, im physischen Speicher verriegelt und mit der entsprechenden Priorität versehen. Diese neu zugewiesene Region wird als aktuelle Region für diese Priorität verwendet. 2. Die Seite wird komprimiert und in der Region gespeichert, wobei sie auf die Granularitätseinheit (16 Bytes) aufgerundet wird. Beispielsweise nimmt eine auf 687 Bytes komprimierte Seite 43 Einheiten zu je 16 Bytes ein. Die Komprimierung erfolgt im aktuellen Thread mit einer niedrigen CPU-Priorität (7), um Störungen zu vermeiden. Ist eine Entkomprimierung erforderlich, so wird sie mithilfe aller verfügbaren Prozessoren parallel ausgeführt. 3. Die Seiten- und Regionsinformationen in den entsprechenden B+-Bäumen werden aktualisiert. 4. Wenn der verbliebene Platz in der aktuellen Region zur Speicherung der komprimierten Seite nicht ausreicht, wird eine neue Region mit derselben Seitenpriorität erstellt und als aktuelle Region für diese Priorität verwendet. Speicherkomprimierung Kapitel 5 521
Wenn eine Seite aus dem Speicher entfernt werden soll, geschieht Folgendes: 1. Der Seiten- und der Regionseintrag werden in den entsprechenden B+-Bäumen gesucht. 2. Die Einträge werden entfernt und die Angaben über den in der Region verwendeten Platz aktualisiert. 3. Wenn die Region leer ist, wird ihre Zuweisung aufgehoben. Durch das ständige Hinzufügen und Entfernen von komprimierten Seiten werden die Regionen im Laufe der Zeit fragmentiert. Der Arbeitsspeicher für eine Region wird erst dann freigegeben, wenn die Region vollständig leer ist. Um Speicherverschwendung zu vermeiden, ist daher eine Form von Verdichtung erforderlich. Sie wird als verzögerte Operation durchgeführt, wobei ihre Stärke vom Grad der Fragmentierung abhängt. Auch die Priorität der Regionen wird dabei berücksichtigt. Experiment: Speicherkomprimierung Bei der Speicherkomprimierung in einem System gibt es nicht viel zu beobachten. Sie können sich jedoch den Speicherkomprimierungsprozess in Process Explorer oder einem Kerneldebugger ansehen. Die folgende Abbildung zeigt die Registerkarte Performance mit den Eigenschaften des Prozesses Memory Compression in Process Explorer (der dazu mit Administratorrechten ausgeführt werden muss): Da in diesem Prozess nur Kernelthreads arbeiten, wird keine Benutzerzeit angezeigt. Außerdem ist der Arbeitssatz rein privat (also nicht gemeinsam nutzbar), da es nicht möglich ist, komprimierten Speicher auf irgendeine Weise mit anderen Prozessen zu teilen. Vergleichen Sie diese Ausgabe mit der Registerkarte Arbeitsspeicher im Task-Manager: → 522 Kapitel 5 Speicherverwaltung
Dieser Screenshot wurde ungefähr eine Minute nach dem vorherigen aufgenommen. Der in Klammern angegebene komprimierte Speicher entspricht dem Arbeitssatz des Speicherkomprimierungssatzes, da dies der Platz ist, den der komprimierte Speicher verbraucht. Speicherpartitionen Virtuelle Maschinen werden herkömmlich dazu verwendet, um Anwendungen oder Gruppen von Anwendungen voneinander getrennt auszuführen. Da VMs nicht miteinander interagieren können, bieten sie starke Sicherheits- und Ressourcengrenzen. Allerdings wird das mit hohen Ressourcenkosten für die Hardware, auf denen die VMs laufen, und mit hohen Verwaltungskosten erkauft. Das war der Grund für das Aufkommen von Containertechnologien wie Docker. Um die Schranken für die Isolierung und die Ressourcenverwaltung niedriger zu setzen, werden dabei auf ein und demselben physischen oder virtuellen Computer Sandboxcontainer für die verschiedenen Anwendungen angelegt. Das Erstellen solcher Container ist schwierig, denn dazu werden Kerneltreiber benötigt, die eine Form von Virtualisierung oberhalb des regulären Windows durchführen, unter anderem die folgenden (wobei ein einziger Treiber alle genannten Funktionen einschließen kann): QQ Ein Dateisystem-(Mini-)Filter, der die Illusion eines isolierten Dateisystems hervorruft QQ Ein Treiber für die Virtualisierung der Registrierung, der die Illusion einer separateren Registrierung hervorruft (CmRegisterCallbacksEx) Speicherpartitionen Kapitel 5 523
QQ Ein privater Objekt-Manager-Namensraum durch Verwendung von Silos (siehe Kapitel 3) QQ Prozessverwaltung zur Verknüpfung von Prozessen mit dem richtigen Container durch Prozesserstellungsbenachrichtigungen (PsSetCreateNotifyRoutineExe) Selbst wenn dieser Treiber vorhanden ist, lassen sich manche Dinge schwer virtualisieren, insbesondere die Speicherverwaltung. Unter Umständen möchte jeder Container seine eigene PFN-Datenbank, seine eigene Auslagerungsdatei usw. verwenden. Die 64-Bit-Versionen von Windows 10 sowie Windows Server 2016 machen dies mithilfe von Speicherpartitionen möglich. Eine Speicherpartition besteht aus ihren eigenen Speicherverwaltungsstrukturen wie Seitenlisten (Standby, geänderte Seiten, Nullseiten, freie Seiten usw.), gesamter zugesicherter Speicher, Arbeitssatz, Seitentrimmer, Schreibthread für geänderte Seiten, Nullseitenthread usw., die von denen der anderen Partitionen getrennt sind. Im System werden Speicherpartitionen durch Partitionsobjekte dargestellt, bei denen es sich wie bei anderen Exekutivobjekten auch um sicherungsfähige und benennbare Objekte handelt. Eine Partition ist immer vorhanden, nämlich die Systempartition, die für das System als Ganzes steht und die Stammpartition aller ausdrücklich erstellten Partitionen ist. Die Adresse der Systempartition ist in der globalen Variablen ­MiSystemPartition gespeichert. Die Partition trägt den Namen KernelObjects\Memory­ Partition0, unter dem sie auch in Werkzeugen wie dem Sysinternals-Tool WinObj angezeigt wird (siehe Abb. 5–45). Abbildung 5–45 Die Systempartition in WinObj Alle Partitionsobjekte sind in einer globalen Liste gespeichert. Die maximale Anzahl von Parti­ tionen beträgt zurzeit 1024 (10 Bits), da der Partitionsindex für den schnellen Zugriff auf die Partitionsinformationen in die PTEs aufgenommen werden muss. Einer dieser Indizes bezeichnet die Systempartition. Zwei weitere Werte werden als Wächter genutzt, sodass Indizes für 1021 Partitionen verbleiben. Speicherpartitionen können im Benutzer- und im Kernelmodus mithilfe der internen, nicht dokumentierten Funktion NtCreatePartition erstellt werden, wobei Aufrufer im Benutzermodus dazu das Recht SeLockMemory benötigen. Die Funktion kann eine Elternpartition entgegennehmen, aus der die ersten Seiten kommen und zu der sie zurückkehren, wenn die neue 524 Kapitel 5 Speicherverwaltung
Partition zerstört wird. Wird keine Elternpartition angegeben, so wird dafür die Systempartition verwendet. NtCreatePartition delegiert die eigentliche Arbeit an die interne Funktion M­ iCreatePartition des Speicher-Managers. Eine bestehende Partition kann anhand ihres Namens mit NtOpenPartition geöffnet werden. Dazu sind keine besonderen Rechte erforderlich, da das Objekt wie üblich durch Zugriffssteuerungslisten geschützt werden kann. Für die Bearbeitung der Partition ist die Funktion NtManagePartition da. Mit ihr können Sie der Partition Speicher und Auslagerungsdateien hinzufügen, Speicher von einer Partition in eine andere kopieren und Informationen über eine Partition abrufen. Experiment: Speicherpartitionen einsehen In diesem Experiment werfen wir mithilfe des Kerneldebuggers einen Blick auf Partitionsobjekte. 1. Starten Sie den lokalen Kerneldebugger und geben Sie den Befehl !partition ein. Er führt alle Partitionsobjekte in dem System auf. lkd> !partition Partition0 fffff803eb5b2480 MemoryPartition0 2. Standardmäßig wird die stets vorhandene Systempartition angezeigt. Sie können bei dem Befehl !partition jedoch auch die Adresse eines Partitionsobjekts angeben, um nähere Einzelheiten darüber abzurufen: lkd> !partition fffff803eb5b2480 PartitionObject @ ffffc808f5355920 (MemoryPartition0) _MI_PARTITION 0 @ fffff803eb5b2480 MemoryRuns: 0000000000000000 MemoryNodeRuns: ffffc808f521ade0 AvailablePages: 0n4198472 ( 16 Gb 16 Mb 288 Kb) ResidentAvailablePages: 0n6677702 ( 25 Gb 484 Mb 792 Kb) 0 _MI_NODE_INFORMATION @ fffff10000003800 TotalPagesEntireNode: 0x7f8885 Zeroed 1GB 0 ( 2MB 41 ( Free 0) 0 (0) 82 Mb) 0 (0) 64KB 3933 ( 245 Mb 832 Kb) 0 (0) 4KB 82745 ( 323 Mb 228 Kb) 0 (0) Node Free Memory: ( 651 Mb 36 Kb ) InUse Memory: ( 31 Gb 253 Mb 496 Kb ) TotalNodeMemory: ( 31 Gb 904 Mb 532 Kb ) → Speicherpartitionen Kapitel 5 525
Die Ausgabe zeigt einige der Informationen, die in der zugrunde liegenden MI_­PARTITIONStruktur gespeichert sind, sowie die Adresse dieser Struktur. Die Angaben sind nach NUMA-­ Knoten gegliedert, wobei es in diesem Beispiel nur einen einzigen gibt. Da dies die Systempartition ist, sollten die Zahlen für den verwendeten, den freien und den Gesamtspeicher den Werten entsprechen, die Sie in Programmen wie dem Task-Manager und Process Explorer ermitteln können. Es ist auch möglich, die MI_PARTITION-Struktur wie üblich mit dem Befehl dt zu untersuchen. Es ist denkbar, dass in Zukunft besondere Prozesse jeweils mit einer Speicherpartition verknüpft werden (mithilfe von Jobobjekten), etwa wenn eine exklusive Kontrolle über den physischen Arbeitsspeicher von Vorteil ist. Eine Anwendung dieser Art bildet bereits der Spielmodus im Creators Update (siehe Kapitel 8 in Band 2). Speicherzusammenführung Der Speicher-Manager greift auf verschiedene Mechanismen zurück, um so viel RAM wie möglich zu sparen, z. B. durch die gemeinsame Nutzung von Seiten für Images, das Kopieren beim Schreiben für Datenseiten und die Komprimierung. In diesem Abschnitt sehen wir uns einen weiteren solchen Mechanismus an, nämlich die Speicherzusammenführung. Die zugrunde liegende Idee ist ganz einfach: Es wird nach mehrfach vorhandenen Seiten im RAM gesucht, die dann zu einer Seite zusammengefasst werden, wobei die nun überflüssig gewordenen Exemplare entfernt werden. Dabei stellen sich jedoch die folgenden Fragen: QQ Was für Seiten sind die »besten« Kandidaten für eine Zusammenführung? QQ Wann ist es angebracht, eine Speicherzusammenführung auszulösen? QQ Soll die Zusammenführung für einen bestimmten Prozess, eine Speicherpartition oder das gesamte System erfolgen? QQ Wie kann der Zusammenführungsprozess so schnell gemacht werden, dass er die reguläre Codeausführung nicht beeinträchtigt? QQ Wie kann eine private Kopie erstellt werden, wenn eine schreibbare zusammengeführte Seite später von einem ihrer Verbraucher geändert wird? All diese Fragen beantworten wir in diesem Abschnitt, wobei wir mit der letzten beginnen. Zur Lösung dieses Problems wird der Copy-on-Write-Mechanismus verwendet: Solange nicht auf die zusammengeführten Seiten geschrieben wird, geschieht nichts. Versucht ein Prozess, auf die Seite zu schreiben, wird für ihn eine Kopie erstellt, wobei das Copy-on-Write-Flag für diese neu zugewiesene private Seite entfernt wird. Die Seitenzusammenführung können Sie abschalten, indem Sie im Registrierungsschlüssel HKLM\System\CurrentControlSet\Control\Session Manager\Memory Management den DWORD-Wert DisablePageCombining festlegen. 526 Kapitel 5 Speicherverwaltung
In diesem Abschnitt verwenden wir die Begriffe CRC und Hash synonym. Beide bezeichnen eine statistisch eindeutige (zumindest mit hoher Wahrscheinlichkeit) 64-Bit-Zahl, die auf den Inhalt einer Seite verweist. Die Initialisierungsroutine MmInitSystem des Speicher-Managers erstellt die Systempartition (siehe den vorhergehenden Abschnitt). In der MI_PARTITION-Struktur, die eine Partition beschreibt, befindet sich ein Array aus 16 AVL-Bäumen, die die Seitendubletten bezeichnen. Das Array ist nach den letzten vier Bits des CRC-Werts der zusammengeführten Seiten sortiert. Sie werden in Kürze sehen, welche Rolle das für den Algorithmus spielt. Es gibt noch zwei besondere Arten von Seiten, die als einfache Seiten bezeichnet werden. Auf den Seiten des einen Typs sind alle Bytes 0, auf denen des anderen Typs 1 (gefüllt mit dem Byte 0xFF). Der CRC wird nur einmal berechnet und dann gespeichert. Bei der Untersuchung der Seiteninhalte lassen sich solche Seiten leicht erkennen. Um eine Speicherzusammenführung auszulösen, wird die native API NtSetSystemInformation mit der Systeminformationsklasse SystemCombinePhysicalMemoryInformation aufgerufen. Dazu muss der Aufrufer das Recht SeProfileSingleProcessPrivilege haben, das gewöhnlich Mitgliedern der lokalen Administratorengruppe gewährt wird. Durch eine Kombination von Argumenten für die API können folgende Optionen eingestellt werden: QQ Speicherzusammenführung in der gesamten (System-)Partition oder nur im aktuellen Prozess QQ Zusammenführung nur von einfachen Seiten (komplett 0 oder 1) oder Zusammenführung von Duplikaten ohne Berücksichtigung ihres Inhalts Es kann optional auch ein Ereignishandle übergeben werden, wobei die Zusammenführung abgebrochen wird, wenn ein anderer Thread das entsprechende Ereignis auslöst. Zurzeit verfügt der Dienst SuperFetch (der am Ende dieses Kapitels eingehender erklärt wird) über einen besonderen Thread, der mit niedriger Priorität (4) läuft und die Speicherzusammenführung für das gesamte System entweder auslöst, wenn der Benutzer nichts tut, oder anderenfalls alle 15 Minuten. Umfasst der physische Speicher mehr als 3,5 GB (3584 MB), wird im Creators Update in jedem Svchost-Prozess meistens ein einziger Dienst ausgeführt. Dadurch werden Dutzende von Prozessen erzeugt, allerdings ist es dadurch nicht mehr wahrscheinlich, dass sich die Dienste gegenseitig beeinflussen (was zu Instabilitäten oder Sicherheitsproblemen führen kann). In dieser Situation verwendet der Dienststeuerungs-Manager (Service Control Manager, SCM) eine neue Option der API für die Speicherzusammenführung und löst alle drei Minuten eine Zusammenführung in jedem der Svchost-Prozesse aus, indem er einen Threadpooltimer mit der Basispriorität 6 verwendet (ScPerformPageCombineOnServiceImages). Damit wird versucht, den RAM-Verbrauch, der jetzt höher sein kann als bei der Verwendung weniger Svchost-Instanzen, wieder zu senken. Für Dienste, die nicht in Svchost-Prozessen ausgeführt werden oder unter benutzerweisen oder privaten Benutzerkonten laufen, erfolgt keine Seitenzusammenführung. Die Routine MiCombineIdenticalPages ist der eigentliche Eintrittspunkt für den Seitenzusammenführungsprozess. Für jeden NUMA-Knoten der Speicherpartition weist sie eine Liste der Seiten einschließlich ihres CRCs zu und speichert sie in der PCS-Struktur (Page Combining Speicherzusammenführung Kapitel 5 527
Support). Dies ist die Struktur zur Verwaltung aller erforderlichen Seitenzusammenführungsoperationen, die auch das zuvor erwähnte Array der AVL-Bäume enthält. Der anfordernde Thread führt die eigentliche Arbeit aus. Da er auf CPUs des aktuellen NUMA-Knotens laufen sollte, wird seine Affinität ggf. entsprechend angepasst. Um die Erklärung des Speicherzusammenführungsalgorithmus zu vereinfachen, teilen wir ihn in drei Phasen auf: Suche, Klassifizierung und eigentliche Zusammenführung. In den folgenden Abschnitten setzen wir voraus, dass eine umfassende Seitenzusammenführung für alle Seiten angefordert wurde, also nicht nur für den aktuellen Prozess und nicht nur für einfache Seiten. In den anderen Fällen läuft der Vorgang nach demselben Prinzip ab, ist aber einfacher. Die Suchphase In der ersten Phase werden die CRCs sämtlicher physischer Seiten berechnet. Der Algorithmus analysiert jede physische Seite, die zur Liste der aktiven oder geänderten Seiten oder zur Standbyliste gehört, überspringt aber mit null initialisierte und freie Seiten, da diese letzten Endes nicht verwendet werden. Gute Kandidaten für eine Speicherzusammenführung sind aktive, nicht gemeinsam genutzte Seiten, die zu einem Arbeitssatz gehören und keiner Auslagerungsstruktur zugeordnet sind. Ein Kandidat kann sich durchaus auf der Standbyliste oder der Liste der geänderten Seiten befinden, muss aber einen Referenzzähler von 0 aufweisen. Grundsätzlich berücksichtigt das System nur drei Arten von Seiten für die Zusammenführung, nämlich solche von Benutzerprozessen, aus dem ausgelagerten Pool und dem Sitzungsraum. Alle anderen Arten von Seiten werden übergangen. Um den CRC einer Seite zu berechnen, muss das System die physische Seite mithilfe eines neuen System-PTEs einer Systemadresse zuordnen, da der Prozesskontext meistens nicht derjenige des aufrufenden Threads ist, weshalb die Seite in den niedrigen Benutzermodusadressen unzugänglich ist. Der CRC der Seite wird dann mit einem eigenen Algorithmus berechnet (MiComputeHash64). Anschließend wird der System-PTE freigegeben, sodass die Seite nicht mehr im Systemadressraum zugeordnet ist. Der Seitenhashalgorithmus Zur Berechnung des 8-Byte-Seitenhashs (CRC) verwendet das System folgenden Algorithmus: Als Ausgangspunkt wird das Produkt zweier großer 64-Bit-Primzahlen verwendet. Die Seite wird dann vom Ende zum Anfang abgetastet, wobei der Algorithmus pro Zyklus 64 Bytes hashcodiert. Jeder auf der Seite gelesene 8-Byte-Wert wird zu dem Ausgangshash hinzugefügt. Anschließend wird der Hash um eine primzahlige Menge von Bits nach rechts gedreht (beginnend mit 2, dann 3, 5, 7, 11, 13, 17 und 19). Um den Hash einer Seite zu bestimmen, sind insgesamt 512 Speicherzugriffsoperationen (4096/8) erforderlich. 528 Kapitel 5 Speicherverwaltung
Die Klassifizierungsphase Wenn alle Hashes der Seiten eines NUMA-Knotens berechnet sind, beginnt der zweite Teil des Algorithmus. In dieser Phase werden die CRC/PFN-Einträge in der Liste verarbeitet und strategisch geschickt sortiert. Der Algorithmus für die eigentliche Zusammenführung muss die Anzahl der Kontextwechsel minimieren und so schnell wie möglich ablaufen. Als Erstes sortiert die Routine MiProcessCrcList die CRC/PFN-Liste mithilfe eines Quicksort-Algorithmus nach den Hashes. Mit dem Zusammenführungsblock, einer weiteren wichtigen Datenstruktur, wird der Überblick über alle Seiten gewahrt, die den gleichen Hash haben. Vor allem aber wird darin der neue Prototyp-PTE für die Zuordnung der neuen, zusammengeführten Seite gespeichert. Anschließend werden die CRC/PFN-Einträge der neuen, sortierten Liste der Reihe nach verarbeitet. Das System muss dabei jeweils prüfen, ob der betrachtete Hash ein einfacher Hash ist, also zu einer Seite gehört, die komplett 0 oder 1 ist, und ob er gleich dem vorhergehenden oder dem nächsten Hash ist (denken Sie daran, dass die Liste nach Hashes sortiert ist). Ist das nicht der Fall, prüft das System, ob in der PCS-Struktur bereits ein Zusammenführungsblock vorhanden ist. Wenn ja, so heißt das, dass bei einer vorherigen Ausführung des Algorithmus oder in einem anderen Knoten des Systems bereits eine zusammengeführte Seite identifiziert wurde. Wenn nein, ist der CRC einmalig, weshalb die Seite nicht zusammengeführt werden kann, sodass der Algorithmus mit der nächsten Seite in der Liste fortfährt. Wird ein einfacher Hash gefunden, der zuvor noch nicht aufgetreten ist, so weist der Algorithmus einen neuen, leeren Zusammenführungsblock für die Master-PFN zu und fügt ihn in eine Liste ein, die dann in der nächsten Phase vom Code für die eigentliche Zusammenführung verwendet wird. Kam der Hash bereits vor – ist die Seite also nicht das Masterexemplar –, wird dem aktuellen CRC/PFN-Eintrag ein Verweis auf den Zusammenführungsblock hinzugefügt. Damit sind jetzt alle Daten vorbereitet, die der Zusammenführungsalgorithmus benötigt: eine Liste von Zusammenführungsblöcken, in denen die physischen Masterseiten und ihre Prototyp-PTEs gespeichert sind, eine Liste der CRC/PFN-Einträge, die nach ihrem Arbeitssatz sortiert sind, und etwas physischer Speicher für die Inhalte der neuen, zusammengeführten Seiten. Anschließend ruft der Algorithmus die Adresse der physischen Seite ab, die laut der zuvor mithilfe von MiCombineIdenticalPage durchgeführten Überprüfung tatsächlich existieren sollte, und sucht nach einer Datenstruktur, in der alle zu dem entsprechenden Arbeitssatz gehörenden Seiten gespeichert sind. Diese Struktur nennen wir im Folgenden WS-CRC-Knoten. Falls sie nicht vorhanden ist, weist der Algorithmus eine neue zu und fügt sie in einen weiteren AVLBaum ein. Der CRC/PFN-Eintrag und die virtuelle Adresse der Seiten werden in dem WS-CRCKnoten miteinander verknüpft. Nachdem alle gefundenen Seiten verarbeitet wurden, weist das System mithilfe einer MDL den physischen Speicher für die neuen Masterseiten zu und verarbeitet die einzelnen WSCRC-Knoten. Aus Leistungsgründen werden die Kandidatenseiten in den Knoten nach ihren ursprünglichen virtuellen Adressen geordnet. Jetzt ist das System bereit für die eigentliche Seitenzusammenführung. Speicherzusammenführung Kapitel 5 529
Die Zusammenführungsphase Zu Anfang der Zusammenführungsphase stehen eine WS-CRC-Knotenstruktur mit all den Seiten, die zu einem bestimmten Arbeitssatz gehören und Kandidaten für eine Zusammenführung sind, sowie eine Liste freier Zusammenführungsblöcke, die zur Speicherung der Prototyp-PTEs und der Masterseiten dient. Der Algorithmus klinkt sich in den Zielprozess ein und verriegelt seinen Arbeitssatz, wobei er dessen IRQL auf Dispatchebene anhebt. Dadurch kann er direkt auf jeder Seite lesen und schreiben, ohne sie neu zuordnen zu müssen. Der Algorithmus verarbeitet alle CRC/PFN-Einträge in der Liste, doch da er auf Dispatch­ ebene läuft und der Vorgang einige Zeit in Anspruch nehmen kann, ruft er vor der Analyse des nächsten Eintrags jeweils KeShouldYieldProcessor auf, um zu prüfen, ob sich DPCs oder geplante Elemente in der Warteschlange des Prozessors befinden. Wenn ja, trifft der Algorithmus Vorsichtsmaßnahmen, um seinen Status zu erhalten, und gibt die Kontrolle über den Prozessor vorübergehend ab. Die Zusammenführungsstrategie geht von folgenden drei möglichen Situationen aus: QQ Die Seite ist aktiv und gültig, enthält aber nur Nullen. Anstatt sie zusammenzuführen, wird ihr PTE durch einen Nullforderungs-PTE ersetzt. Dies ist der ursprüngliche Zustand bei einer Speicherzuweisung mit VirtualAlloc oder vergleichbaren Funktionen. QQ Die Seite ist aktiv und gültig, aber nicht mit Nullen initialisiert, was bedeutet, dass sie zusammengeführt werden muss. Der Algorithmus prüft, ob die Seite zum Master befördert wurde. Das ist der Fall, wenn der CRC/PFN-Eintrag nicht über einen Zeiger auf einen gültigen Zusammenführungsblock verfügt. In dieser Situation wird der Hash der Masterseite erneut überprüft und eine neue physische Seite für die gemeinsame Nutzung zugewiesen. Anderenfalls wird der vorhandene Zusammenführungsblock verwendet und dessen Referenzzähler heraufgesetzt. Das System ist jetzt bereit, die private in eine gemeinsam genutzte, zusammengeführte Seite umzuwandeln, und ruft dazu MiConvertPrivateToProto auf. QQ Die Seite befindet sich auf der Standbyliste oder der Liste der geänderten Seiten. In diesem Fall wird sie als gültige Seite einer Systemadresse zugeordnet und ihr Hash neu berechnet. Der Algorithmus führt dieselben Schritte aus wie in der vorherigen Situation, wobei der PTE allerdings mit MiConvertStandbyToProto von einem gemeinsamen zu einem Prototyp-PTE umgewandelt wird. Am Ende der Zusammenführung der aktuellen Seite führt das System den Zusammenführungsblock des Masterexemplars in die PCS-Struktur ein. Das ist wichtig, da dieser Blick die Verbindung zwischen den einzelnen privaten PTEs und der zusammengeführten Seite bildet. Vom privaten zum gemeinsamen PTE Die Routine MiConvertPrivateToProto dient dazu, den PTE einer aktiven und gültigen Seite umzuwandeln. Wenn sie feststellt, dass der Prototyp-PTE im Zusammenführungsblock 0 ist, so bedeutet das, dass die Masterseite zusammen mit dem gemeinsamen Prototyp-PTE erstellt werden muss. Anschließend ordnet sie die freie physische Adresse einer Systemadresse zu und kopiert den Inhalt der privaten Seite in die neue, gemeinsam genutzte. Bevor der gemeinsame 530 Kapitel 5 Speicherverwaltung
PTE erstellt wird, sollte das System alle Reservierungen in der Auslagerungsdatei freigeben (siehe den entsprechenden Abschnitt weiter vorn in diesem Kapitel) und den PFN-Deskriptor der gemeinsam genutzten Seite ausfüllen. In der PFN für die gemeinsam genutzte Seite ist das Prototypbit gesetzt, und der PTE-Framezeiger verweist auf die PFN der physischen Seite mit dem Prototyp-PTE. Vor allem aber weist der PTE-Zeiger auf den PTE innerhalb des Zusammenführungsblocks, wobei allerdings das 63. Bit auf 0 gesetzt ist. Daran kann das System erkennen, dass die PFN zu einer zusammengeführten Seite gehört. Als Nächstes muss das System den PTE der privaten Seite ändern, indem es die Ziel-PFN darin auf die gemeinsam genutzte physische Seite setzt, den Schutz auf Kopieren beim Schreiben einstellt und den PTE der privaten Seite als gültig kennzeichnet. Auch der Prototyp-PTE im Zusammenführungsblock wird als gültiger Hardware-PTE gekennzeichnet, denn der Inhalt ist identisch mit dem neuen PTE der privaten Seite. Schließlich wird der für die private Seite zugewiesene Platz in der Auslagerungsdatei freigegeben und die ursprüngliche PFN der privaten Seite als gelöscht markiert. Der TLB-Cache wird auf die Festplatte geleert, und der Arbeitssatz des privaten Prozesses wird um eine Seite verkleinert. Ist der Prototyp-PTE im Zusammenführungsblock dagegen nicht 0, so heißt das, dass die private Seite eine Kopie der Masterseite sein soll. In diesem Fall muss nur der aktive PTE der privaten Seite umgewandelt werden. Die PFN der gemeinsam genutzten Seite wird einer Systemadresse zugeordnet und der Inhalt der beiden Seiten wird verglichen. Das ist wichtig, da der CRC-Algorithmus im Allgemeinen keine eindeutigen Werte hervorruft. Stimmen die beiden Seiten nicht überein, bricht die Funktion die Verarbeitung ab und gibt die Steuerung zurück. Anderenfalls hebt sie die Zuordnung der gemeinsam genutzten Seite auf und legt für die gemeinsam genutzte PFN die höhere der beiden Seitenprioritäten fest. Abb. 5–46 zeigt den Zustand in dem Fall, dass nur eine Masterseite existiert. Zusammen­führungs­block Prozess A Prototyp-PTE PFN = 0x7ffd0 Gültig, Copy-on-Write PTE Seite PFN = 0x7ffd0 Gültig, Copy-on-Write PTE-Adresse: 0x7fffb284`74a09f48 Zähler für gemeinsame Nutzung: Aktiv, gem. genutzt, zusammengeführt PFN-Eintrag Abbildung 5–46 Zusammengeführte Masterseite Jetzt berechnet der Algorithmus den neuen ungültigen Software-Prototyp-PTE, der in die private Seitentabelle des Prozesses eingefügt werden muss. Dazu liest er im Zusammenführungsblock die Adresse des Hardware-PTEs, der die gemeinsam genutzte Seite zuordnet, verschiebt sie und setzt das Prototyp- und das Zusammenführungsbit. Anschließend prüft er, ob der Zähler für die gemeinsame Nutzung der privaten PFN 1 ist. Wenn nein, wird die Verarbeitung ab- Speicherzusammenführung Kapitel 5 531
gebrochen. Der Algorithmus schreibt den neuen Software-PTE in die private Seitentabelle des Prozesses und setzt den Zähler für die gemeinsame Nutzung in der Seitentabelle für die alte private PFN. (Denken Sie daran, dass eine aktive PFN immer einen Zeiger auf ihre Seitentabelle unterhält.) Der Arbeitssatz des Zielprozesses wird um eine Seite verkleinert und der TLB auf die Festplatte geleert. Die alte private Seite wird in den Übergangszustand versetzt und ihre PFN zur Löschung markiert. Abb. 5–47 zeigt zwei Seiten, von denen die neue auf den Prototyp-PTE zeigt, aber noch nicht gültig ist. Um zu verhindern, dass gemeinsam genutzte Seiten bei der Verkleinerung des Arbeitssatzes entfernt werden, wendet das System einen Trick an, nämlich die Simulation eines Seitenfehlers. Dadurch wird der Zähler für die gemeinsame Nutzung der gemeinsamen PFN wieder erhöht, und es tritt kein Fehler auf, wenn der Prozess versucht, die gemeinsame Seite zu lesen. Das führt letzten Endes dazu, dass der private PTE wieder ein gültiger Hardware-PTE wird. Abb. 5–48 zeigt die Auswirkungen eines weichen Seitenfehlers auf die zweite Seite: Sie wird dadurch gültig gemacht und ihr Zähler für die gemeinsame Nutzung wird heraufgesetzt. Prozess B (oder A) PTE Seite Nicht gültig, zusammengeführt Proto-Adresse: FFFFB28474A09F80 Zusammen­führungs­block Prozess A Prototyp-PTE PFN = 0x7ffd0 Gültig, Copy-on-Write PTE Seite PFN = 0x7ffd0 Gültig, Copy-on-Write PTE-Adresse: 0x7fffb284`74a09f48 Zähler für gemeinsame Nutzung = 1 Aktiv, gem. genutzt, zusammengeführt PFN-Eintrag Abbildung 5–47 Zusammengeführte Seiten vor einem simulierten Seitenfehler 532 Kapitel 5 Speicherverwaltung
Prozess B (oder A) PTE Seite PFN = 0x7ffd0 Gültig, Copy-on-Write Zusammen­führungs­block Prozess A Prototyp-PTE Gültig, PFN = 0x7ffd0 Gültig, Copy-on-Write PTE Seite PFN = 0x7ffd0 Gültig, Copy-on-Write PTE-Adresse: 0x7fffb284`74a09f48 Zähler für gemeinsame Nutzung = 2 Aktiv, gem. genutzt, zusammengeführt PFN-Eintrag Abbildung 5–48 Zusammengeführte Seiten nach einem simulierten Seitenfehler Freigabe von zusammengeführten Seiten Wenn das System eine bestimmte virtuelle Adresse freigeben muss, sucht es als Erstes die Adresse des PTEs, der sie zuordnet und der wiederum auf eine PFN zeigt. Bei der PFN einer zusammengeführten Seite sind das Prototyp- und das Zusammenführungsbit gesetzt. Die Freigabeanforderung für eine zusammengeführte PFN wird genauso verwaltet wie die für eine Prototyp-PFN. Der einzige Unterschied besteht darin, dass das System bei gesetztem Zusammenführungsbit nach der Verarbeitung der Prototyp-PFN noch MiDecrementCombinedPte aufruft. MiDecrementCombinedPte ist eine einfache Funktion, die den Referenzzähler im Zusammenführungsblock des Prototyp-PTEs heruntersetzt. (In diesem Stadium befindet sich der PTE im Übergang, da der Speicher-Manager die Zuweisung der zugeordneten physischen Seite bereits aufgehoben hat. Der Zähler für die gemeinsame Verwendung der physischen Seite ist bereits auf 0 gefallen, weshalb das System den PTE in den Übergangszustand versetzt hat.) Fällt der Referenzzähler auf 0, wird der Prototyp-PTE freigegeben, die physische Seite auf die Liste der freien Seiten gestellt und der Zusammenführungsblock an die Liste freier zusammengeführter Seiten in der PCS-Struktur der Speicherpartition zurückgegeben. Speicherzusammenführung Kapitel 5 533
Experiment: Speicherzusammenführung In diesem Experiment beobachten Sie die Auswirkungen der Speicherzusammenführung. Gehen Sie dazu wie folgt vor: 1. Starten Sie eine Kerneldebuggingsitzung mit einer VM als Ziel (siehe Kapitel 4). 2. Kopieren Sie MemCombine32.exe (für 32-Bit-Ziele) oder MemCombine64.exe (für 64-Bit-Ziele) sowie MemCombineTest.exe aus den Begleitmaterialien zu diesem Buch auf den Zielrechner. 3. Führen Sie auf dem Zielcomputer MemCombineTest.exe aus. Sie sehen jetzt folgende Anzeige: 4. Beachten Sie die beiden Adressen. Dies sind zwar Puffer, die mit zufällig generierten Bytemustern gefüllt sind. Die Muster werden wiederholt, sodass jede Seite den gleichen Inhalt hat. 5. Suchen Sie im Debugger den Prozess MemCombineTest: 0: kd> !process 0 0 memcombinetest.exe PROCESS ffffe70a3cb29080 SessionId: 2 Cid: 0728 Peb: 00d08000 ParentCid: 11c4 DirBase: c7c95000 ObjectTable: ffff918ede582640 HandleCount: <Data Not Accessible> Image: MemCombineTest.exe 6. Schalten Sie zu diesem Prozess um: 0: kd> .process /i /r ffffe70a3cb29080 You need to continue execution (press 'g' <enter>) for the context to be switched. When the debugger breaks in again, you will be in the new process context. 0: kd> g Break instruction exception - code 80000003 (first chance) nt!DbgBreakPointWithStatus: fffff801'94b691c0 cc int 3 → 534 Kapitel 5 Speicherverwaltung
7. Suchen Sie mit !pte die PFN, in der die beiden Puffer gespeichert sind: 0: kd> !pte b80000 VA 0000000000b80000 PXE at FFFFA25128944000 PPE at FFFFA25128800000 PDE at FFFFA25100000028 PTE at FFFFA20000005C00 contains 00C0000025BAC867 contains 0FF00000CAA2D867 contains 00F000003B22F867 contains B9200000DEDFB867 pfn 25bac ---DA--UWEV pfn caa2d ---DA--UWEV pfn 3b22f ---DA-UWEV pfn dedfb ---DA--UW-V 0: kd> !pte b90000 VA 0000000000b90000 PXE at FFFFA25128944000 PPE at FFFFA25128800000 PDE at FFFFA25100000028 PTE at FFFFA20000005C80 contains 00C0000025BAC867 contains 0FF00000CAA2D867 contains 00F000003B22F867 contains B9300000F59FD867 pfn 25bac ---DA--UWEV pfn caa2d ---DA--UWEV pfn 3b22f ---DA-UWEV pfn f59fd ---DA--UW-V 8. Die PFN-Werte sind unterschiedlich, was anzeigt, dass diese Seiten unterschiedlichen physischen Adressen zugeordnet sind. Nehmen Sie die Ausführung des Ziels wieder auf. 9. Öffnen Sie auf dem Zielcomputer eine Eingabeaufforderung mit erhöhten Rechten, wechseln Sie zu dem Verzeichnis, in das Sie MemCombine(32/64) kopiert haben, und führen Sie diese Datei aus. Das Programm erzwingt eine vollständige Speicherzusammenführung, was einige Sekunden dauern kann. 10. Wenn die Speicherzusammenführung abgeschlossen ist, klinken Sie sich wieder mit dem Debugger ein. Wiederholen Sie die Schritte 6 und 7. Jetzt sehen Sie die geänderten PFNs: 1: kd> !pte b80000 VA 0000000000b80000 PXE at FFFFA25128944000 PPE at FFFFA25128800000 PDE at FFFFA25100000028 PTE at FFFFA20000005C00 contains 00C0000025BAC867 contains 0FF00000CAA2D867 contains 00F000003B22F867 contains B9300000EA886225 pfn 25bac ---DA--UWEV pfn caa2d ---DA--UWEV pfn 3b22f ---DA-UWEV pfn ea886 C---A--UR-V 1: kd> !pte b90000 VA 0000000000b90000 PXE at FFFFA25128944000 PPE at FFFFA25128800000 PDE at → Speicherzusammenführung Kapitel 5 535
FFFFA25100000028 PTE at FFFFA20000005C80 contains 00C0000025BAC867 contains 0FF00000CAA2D867 contains 00F000003B22F867 contains BA600000EA886225 pfn 25bac ---DA--UWEV pfn caa2d ---DA--UWEV pfn 3b22f ---DA-UWEV pfn ea886 C---A--UR-V 11. Die PFN-Werte sind identisch, da die Seiten derselben Adresse im RAM zugeordnet sind. Beachten Sie auch das Flag C in der PFN, das für Copy-on-Write steht. 12. Nehmen Sie die Ausführung des Ziels wieder auf und drücken Sie im Fenster von MemCombineTest eine beliebige Taste. Dadurch wird ein einziges Byte im ersten Puffer geändert. 13. Klinken Sie sich wieder ein und führen Sie abermals Schritt 6 und 7 aus: 1: kd> !pte b80000 VA 0000000000b80000 PXE at FFFFA25128944000 PPE at FFFFA25128800000 PDE at FFFFA25100000028 PTE at FFFFA20000005C00 contains 00C0000025BAC867 contains 0FF00000CAA2D867 contains 00F000003B22F867 contains B9300000813C4867 pfn 25bac ---DA--UWEV pfn caa2d ---DA--UWEV pfn 3b22f ---DA-UWEV pfn 813c4 ---DA--UW-V 1: kd> !pte b90000 VA 0000000000b90000 PXE at FFFFA25128944000 PPE at FFFFA25128800000 PDE at FFFFA25100000028 PTE at FFFFA20000005C80 contains 00C0000025BAC867 contains 0FF00000CAA2D867 contains 00F000003B22F867 contains BA600000EA886225 pfn 25bac ---DA--UWEV pfn caa2d ---DA--UWEV pfn 3b22f ---DA-UWEV pfn ea886 C---A--UR-V 14. Die PFN des ersten Puffers hat sich geändert, und das Copy-on-Write-Flag wurde entfernt. Die Seite wurde geändert und an eine andere Adresse im RAM verlegt. Speicherenklaven In einem Prozess ausgeführte Threads haben Zugriff auf den gesamten Prozessadressraum ( je nach Seitenschutz, der aber geändert werden kann). Das ist zwar meistens wünschenswert, doch wenn Schadcode sich in einen Prozess einschleusen kann, stehen ihm die gleichen Möglichkeiten zur Verfügung: Er kann ungehindert Daten lesen, die möglicherweise sensible Informationen umfassen, und sie sogar ändern. Intel hat die SGX-Technologie entwickelt (Software Guard Extensions), mit der sich geschützte Speicherenklaven erstellen lassen. Dies sind sichere Zonen im Prozessadressraum, in 536 Kapitel 5 Speicherverwaltung
denen die CPU Code und Daten vor Code schützt, der außerhalb der Enklave läuft. Umgekehrt jedoch hat der Code in einer Enklave den normalen Vollzugriff auf den Prozessadressraum außerhalb. Natürlich ist die Enklave darüber hinaus auch gegen Zugriffe von anderen Prozessen und sogar von Kernelmoduscode geschützt. Abb. 5–49 zeigt eine vereinfachte Darstellung einer Speicherenklave. Geschützte Enklave Geschützte Daten Vollzugriff Kein Zugriff Geschützter Code Code Daten Abbildung 5–49 Speicherenklaven Intel SGX wird von Core-Prozessoren der 6. Generation (Skylake) und höher unterstützt. Intel stellt ein eigenes SDK für Anwendungsentwickler bereit, das für Windows 7 und höhere Systeme genutzt werden kann (nur 64 Bit). Ab Windows 10 Version 1511 und Windows Server 2016 bietet Windows eine Abstraktion mithilfe von Windows-API-Funktionen, wodurch es nicht mehr nötig ist, das Intel-SDK zu verwenden. Möglicherweise werden andere CPU-Hersteller in Zukunft ähnliche Lösungen entwickeln, die dann über dieselbe API gehandhabt werden können. Dadurch ergibt sich eine relativ gut portierbare Schicht, die Anwendungsentwickler nutzen können, um Enklaven zu erstellen und zu füllen. Nicht alle Core-Prozessoren der 6. Generation unterstützen SGX. Außerdem muss auf dem System ein entsprechendes BIOS-Update installiert werden, damit SGX funktionieren kann. Mehr erfahren Sie in der Dokumentation zu Intel SGX. Die SGX-Website erreichen Sie auf https://software.intel.com/en-us/sgx. Intel stellt zwei Versionen von SGX her (1.0 und 2.0), wobei Windows zurzeit nur Version 1.0 unterstützt. Die Unterschiede zwischen den Versionen würden den Rahmen dieses Buches sprengen. Schlagen Sie dazu in der SGX-Dokumentation nach. Bei den aktuellen SGX-Versionen sind keine Enklaven in Ring 0 (Kernelmodus) möglich, sondern nur in Ring 3 (Benutzermodus). Speicherenklaven Kapitel 5 537
Programmierschnittstellen Um in ihren Programmen Enklaven zu erstellen und zu verwenden, müssen Anwendungsentwickler wie folgt vorgehen (wobei die internen Mechanismen, die dabei ablaufen, in den folgenden Abschnitten dargestellt sind): 1. Als Erstes muss das Programm feststellen, ob Speicherenklaven überhaupt unterstützt werden. Dazu ruft es IsEnclaveTypeSupported auf und übergibt dabei einen Wert für die gewünschte Enklaventechnologie. Zurzeit ist dabei nur der Wert ENCLAVE_TYPE_SGX möglich. 2. Um eine neue Enklave zu erstellen, wird die Funktion CreateEnclave aufgerufen, die ähnliche Argumente verwendet wie VirtualAllocEx. Beispielsweise ist es möglich, eine Enklave in einem anderen Prozess aus dem Aufruferprozess zu erstellen. Kompliziert wird die Sache dadurch, dass eine herstellerspezifische Konfigurationsstruktur benötigt wird. Für Intel ist das eine 4 KB große SGX-Enklavensteuerungsstruktur (SGX Enclave Control Structure, SECS), die Microsoft nicht ausdrücklich definiert. Stattdessen müssen Entwickler ihre eigene Struktur für die jeweils verwendete Technologie anhand ihrer Dokumentation erstellen. 3. Nachdem eine leere Enklave erstellt ist, besteht der nächste Schritt darin, sie mit Code und Daten von außerhalb zu füllen. Dazu wird die Funktion LoadEnclaveData aufgerufen, die Daten im Speicher außerhalb der Enklave in die Enklave hineinkopiert. Es kann durchaus mehrere Aufrufe dieser Funktion geben. 4. Abschließend muss die Enklave mit InitializeEnclave aktiviert werden. Code, der zur Ausführung in der Enklave eingerichtet wurde, kann jetzt starten. 5. Leider gibt es keine API für die Ausführung von Code in der Enklave. Stattdessen ist eine unmittelbare Verwendung von Assembleranweisungen erforderlich. EENTER überträgt die Ausführung an die Enklave, und EEXIT veranlasst den Rücksprung zur aufrufenden Funktion. Eine irreguläre Beendigung der Enklavenausführung, z. B. aufgrund eines Fehlers, ist mit AEX möglich (Asynchronous Enclave Exit). Die Einzelheiten sind nicht Windows-spezifisch und daher nicht Thema dieses Buches. Richten Sie sich dazu nach der SGX-Dokumentation. 6. Um eine Enklave zu zerstören, kann eine normale VirtualFree(Ex)-Funktion auf den in CreateEnclave gewonnenen Zeiger auf die Enklave angewendet werden. Initialisierung von Speicherenklaven Beim Systemstart ruft Winload (der Windows-Bootloader) OslEnumerateEnclavePageRegions auf. Diese Funktion wendet als Erstes die Funktion CPUID an, um zu prüfen, ob SGX unterstützt wird. Wenn ja, gibt sie Anweisungen aus, um die EPC-Deskriptoren aufzulisten. Ein EPC (Enclave Page Cache) ist der geschützte Speicher, den der Prozessor zum Erstellen und Verwenden von Enklaven bereitstellt. Für jeden aufgelisteten EPC ruft OslEnumerateEnclavePageRegions die Funktion BlMmAddEnclavePageRange auf, um Informationen über den Seitenbereich zu einer sortierten Liste von Speicherdeskriptoren mit dem Typwert LoaderEnclaveMemory hinzuzufügen. Diese Lis- 538 Kapitel 5 Speicherverwaltung
te wird schließlich im Element MemoryDescriptorListHead der LOADER_PARAMETER_BLOCK-Struktur gespeichert, mit der Informationen vom Bootloader an den Kernel übergeben werden. Während der Initialisierung wird die Speicher-Manager-Routine MiCreateEnclaveRegions aufgerufen, um einen AVL-Baum für die ermittelten Enklavenregionen zu erstellen, sodass sie bei Bedarf schnell nachgeschlagen werden können. Dieser Baum wird im Datenelement ­MiState.Hardware.EnclaveRegions gespeichert. Der Kernel fügt eine neue Enklavenseitenliste hinzu. Ein besonderes Flag, das an MiInsertPageInFreeOrZeroedList übergeben wird, ermöglicht es, diese neue Liste zu nutzen. Da der Speicher-Manager jedoch keine Listen-IDs mehr zur Verfügung hat (es werden drei Bits verwendet, sodass höchstens acht Werte möglich sind, und alle davon sind bereits verwendet), hält der Kernel diese Seiten für »unzulässige« Seiten, bei denen ein Einlagerungsfehler aufgetreten ist. Da der Speicher-Manager niemals unzulässige Seiten verwendet, wird er dadurch davon abgehalten, die Enklavenseiten für normale Speicherverwaltungsoperationen zu verwenden. Aufbau von Enklaven Die API CreateEnclave ruft schließlich NtCreateEnclave im Kernel auf. Wie bereits erwähnt, muss eine SECS-Struktur übergeben werden. Ihr Aufbau ist in der SGX-Dokumentation von Intel beschrieben. Einen Überblick sehen Sie in Tabelle 5–24. NtCreateEnclave prüft zunächst, ob Speicherenklaven unterstützt werden, indem sie sich den Stamm des AVL-Baums ansieht (also nicht mithilfe der langsameren Anweisung CPUID). Anschließend erstellt sie eine Kopie der übergebenen Struktur, wie es bei Kernelfunktionen üblich ist, die Daten vom Benutzermodus erhalten. Falls sich die Enklave in einem anderen Prozess als dem des Aufrufers befindet, hängt sie diese Struktur an den Zielprozess an (­KeStackAttachProcess). Anschließend überträgt sie die Steuerung an MiCreateEnclave, die die eigentliche Arbeit ausführt. Feld Offset (Bytes) Größe (Bytes) Beschreibung SIZE 0 8 Größe der Enklave in Bytes; muss eine Zweierpotenz sein. BASEADDR 8 8 Lineare Basisadresse der Enklave; muss natürlich an der Größe ausgerichtet sein. SSAFRAMESIZE 16 4 Größe eines SSA-Frames auf den Seiten (einschließlich XSAVE, Auffüllung, GPR und optional MISC) MRSIGNER 128 32 Messregister, erweitert mit dem öffentlichen Schlüssel, der die Enklave verifiziert hat. Korrektes Format siehe SIGSTRUCT. RESERVED 160 96 ISVPRODID 256 2 Produkt-ID der Enklave ISVSVN 258 2 Sicherheitsversionsnummer (SVN) der Enklave EID Implementierungsabhängig 8 Enklaven-ID → Speicherenklaven Kapitel 5 539
Feld Offset (Bytes) Größe (Bytes) Beschreibung PADDING Implementierungsabhängig 352 Auffüllmuster von der Signatur (verwendet für Schlüsselableitungsstrings) RESERVED 260 3836 Schließt EID und andere von null verschiedene reservierte Felder und muss von null verschieden sein. Tabelle 5–24 Aufbau einer SECS-Struktur Als Erstes weist MiCreateEnclave die AWE-Informationsstruktur zu (Address Windowing Extension), die auch die AWE-APIs nutzen, weil Enklaven ähnlich wie AWE einen direkten Zugriff von Benutzermodusanwendungen auf physische Seiten zulassen (genauer ausgedrückt, auf die physischen Seiten, die bei der zuvor beschriebenen Überprüfung als EPC-Seiten erkannt wurden). Wenn Benutzermodusanwendungen die direkte Kontrolle über physische Seiten haben, müssen stets AWE-Datenstrukturen und -Sperren verwendet werden. Diese Datenstruktur wird im Feld AweInfo der EPROCESS-Struktur gespeichert. Als Nächstes ruft MiCreateEnclave die Funktion MiAllocateEnclaveVad auf, um einen Enklaven-VAD zuzuweisen, der den virtuellen Speicherbereich der Enklave beschreibt. Bei diesem VAD ist wie bei allen AWE-VADs das Flag VadAwd gesetzt, aber auch das zusätzliche Flag Enclave, was ihn von einem echten AWE-VAD unterscheidet. Im Rahmen der VAD-Zuweisung wird schließlich auch die Benutzermodusadresse für den Arbeitsspeicher der Enklave ausgewählt (falls sie nicht schon ausdrücklich im ursprünglichen Aufruf von CreateEnclave angegeben war). Im nächsten Schritt erwirbt MiCreateEnclave eine Enklavenseite unabhängig von der Enklavengröße und der ursprünglichen Zusicherung, da laut der SGX-Dokumentation von Intel alle Enklaven über eine mindestens einseitige Steuerstruktur verfügen müssen. Um die erforderliche Zuweisung zu gewinnen, wird MiGetEnclavePage verwendet. Sie untersucht die zuvor beschriebene Enklavenseitenlisten und entnimmt ihr nach Bedarf eine Seite. Diese zurückgegebene Seite wird mit einem System-PTE zugeordnet, der im Enklaven-VAD gespeichert ist. Die Funktion MiInitializeEnclavePfn richtet die zugehörige PFN-Datenstruktur ein und kennzeichnet sie mit Modified und ActiveAndValid. Es gibt keine Bits, an denen man die Enklaven-PFN von einer anderen aktiven Region im Speicher unterscheiden könnte, etwa vom nicht ausgelagerten Pool. Hier kommt der AVLBaum für Enklavenregionen ins Spiel. Wenn der Kernel prüfen muss, ob eine PFN eine EPC-­ Region beschreibt, verwendet er dazu die Funktion MI_PFN_IS_ENCLAVE. Nach der Initialisierung der PFN wird der System-PTE in einen globalen Kernel-PTE umgewandelt und die resultierende virtuelle Adresse berechnet. Der letzte Schritt von ­MiCreateEnclave besteht darin, KeCreateEnclave aufzurufen, die die maschinennahen Schritte zur Enklavenerstellung im Kernel ausführt und sich dabei auch um die Kommunikation mit der SGX-Hardwareimplementierung kümmert. Unter anderem ist KeCreateEnclave dafür verantwortlich, die Basisadresse anzugeben, wenn der Aufrufer keine angegeben hat. Diese Adresse muss in der SECS-Struktur vorhanden sein, bevor die Kommunikation mit der SGX-Hardware zum Erstellen der Enklave beginnt. 540 Kapitel 5 Speicherverwaltung
Daten in eine Enklave laden Wenn die Enklave erstellt ist, muss sie mit Informationen geladen werden. Dazu ist die Funktion LoadEnclaveData da, die die entsprechende Anforderung aber nur an die zugrunde liegende Exekutivfunktion NtLoadEnclaveData weiterleitet. Diese wiederum führt eine Form von Speicherkopieroperation mit einigen VirtualAlloc-Attributen durch, etwa dem Seitenschutz. Enthält die Enklave noch keine zugesicherten Enklavenseiten, müssen diese erst erworben werden. Dadurch wird mit Nullen überschriebener Speicher zu der Enklave hinzugefügt, der dann mit nicht mit Nullen gefülltem Speicher außerhalb der Enklave gefüllt werden kann. Wurde dagegen eine Initialisierungsgröße mit vorab zugesicherten Seiten übergeben, können diese unmittelbar mit Nicht-Null-Speicher außerhalb der Enklave gefüllt werden. Da der Enklavenspeicher durch einen VAD beschrieben wird, funktionieren viele der herkömmlichen Speicherverwaltungs-APIs auch bei diesem Speicher; zumindest teilweise. Beispielsweise führt ein Aufruf von VirtualAlloc (der schließlich bei NtAllocateVirtualMemory landet) mit dem Flag MEM_COMMIT für eine solche Adresse dazu, dass MiCommitEnclavePages aufgerufen wird. Diese Funktion wiederum überprüft, ob die Schutzmaske für die neuen Seiten kompatibel ist (das heißt, ob sie die richtige Kombination aus Lese-, Schreib- und Ausführungsflags enthält, wobei besondere Flags für Caching und kombiniertes Schreiben nicht berücksichtigt werden), und ruft dann MiAddPagesToEnclave auf. Dabei übergibt sie einen Zeiger auf den Enklaven-VAD für den Adressbereich, die in VirtualAlloc angegebene Schutzmaske und die PTE-Adressen für den zuzusichernden virtuellen Adressbereich. MiAddPagesToEnclave prüft als Erstes, ob mit dem Enklaven-VAD vorhandene EPC-Seiten verknüpft sind und ob es genug davon gibt, um die Zusicherung zu erfüllen. Wenn nicht, wird MiReserveEnclavePages aufgerufen, um eine ausreichende Menge zu beziehen. Diese Funktion schaut sich die aktuelle Enklavenseitenliste an und bestimmt die Summe. Stellt der Prozessor nicht genügend physische EPC-Seiten bereit (nach den beim Hochfahren ermittelten Informa­ tionen), schlägt die Funktion fehl. Anderenfalls ruft MiReserveEnclavePages die Funktion MiGetEnclavePage in einer Schleife für die erforderliche Menge an Seiten auf. Jeder abgerufene PFN-Eintrag wird mit dem PFN-Array im Enklaven-VAD verknüpft. Das heißt, wenn eine Enklaven-PFN aus der Enklavenseitenliste herausgenommen und in den aktiven Status versetzt wurde, fungiert der Enklaven-VAD als Liste der aktiven Enklaven-VADs. Sobald die erforderlichen zugesicherten Seiten bezogen wurden, übersetzt MiAddPagesToEnclave den an LoadEnclaveData übergebenen Seitenschutz in die SGX-Gegenstücke. Anschließend reserviert die Funktion die entsprechende Anzahl von System-PTEs, um Informationen über die einzelnen erforderlichen EPC-Seiten festzuhalten. Danach ruft sie schließlich KeAddEnclavePage auf, die wiederum die SGX-Hardware aufruft, um die Seiten tatsächlich hinzuzufügen. Das besondere Seitenschutzattribut PAGE_ENCLAVE_THREAD_CONTROL gibt an, dass der Speicher für eine von SGX definierte Threadsteuerstruktur (Thread Control Structure, TCS) gebraucht wird. Jede TCS steht für einen eigenen Thread, der für sich allein in der Enklave ausgeführt werden kann. NtLoadEnclaveData überprüft Parameter und ruft dann MiCopyPagesIntoEnclave auf, um die eigentliche Arbeit auszuführen. Dazu kann es gehören, zugesicherte Seiten abzurufen, wie es bereits weiter vorn erklärt wurde. Speicherenklaven Kapitel 5 541
Eine Enklave initialisieren Nachdem die Enklave erstellt und mit Daten versehen wurde, ist noch ein letzter Schritt erforderlich, bevor in der Enklave Code ausgeführt werden kann. Die Funktion InitializeEnclave muss SGX darüber benachrichtigen, dass sich die Enklave im letzten Stadium vor der Ausführung befindet. Ihr müssen die beiden SGX-spezifischen Strukturen SIGSTRUCT und EINITTOKEN übergeben werden (siehe die SGX-Dokumentation). Die von InitializeEnclave aufgerufene Exekutivfunktion NtInitializeEnclave überprüft einige Parameter und stellt sicher, dass der Enklaven-VAD über die richtigen Attribute verfügt. Anschließend übergibt sie die Strukturen an die SGX-Hardware. Beachten Sie, dass eine Enklave nur einmal initialisiert werden kann. Jetzt kann die Codeausführung mithilfe der Intel-Assembleranweisung EENTER gestartet werden (siehe SGX-Dokumentation). Vorausschauende Speicherverwaltung (SuperFetch) Bei der herkömmlichen Speicherverwaltung in Betriebssystemen liegt der Schwerpunkt auf dem bis jetzt beschriebenen Modell der bedarfsweisen Auslagerung mit einigen Erweiterungen wie Clustering und Prefetching, die eine Optimierung der Festplatten-E/A zum Zeitpunkt des Seitenfehlers ermöglichen. Clientversionen von Windows enthalten jedoch noch eine erhebliche Verbesserung der Verwaltung von physischem Speicher, nämlich SuperFetch. Dabei handelt es sich um ein Speicherverwaltungsverfahren, das das Prinzip des am weitesten zurückliegenden Zugriffs durch Verlaufsinformationen über den Dateizugriff und vorausschauende Speicherverwaltung verstärkt. Bei der Verwaltung der Standbyliste in älteren Windows-Versionen gab es zwei Einschränkungen. Erstens basierte die Priorität der Seiten nur auf dem unmittelbar vorausgegangenen Verhalten der Prozesse und nahm keine künftigen Speicheranforderungen vorweg. Zweitens waren die Daten für die Vergabe der Prioritäten auf die Seiten beschränkt, die dem Prozess zum jeweiligen Zeitpunkt gehörten. Diese Mängel konnten dazu führen, dass auf einem Computer, auf dem speicherintensive Systemanwendungen wie Virenscans oder eine Diskfragmentierung ausgeführt wurden, während der Benutzer kurzzeitig nicht darauf gearbeitet hat, anschließende interaktive Anwendungen sehr langsam laufen oder langsam gestartet werden. Das kann auch geschehen, wenn ein Benutzer eine daten- oder speicherintensive Anwendung ausführt und dann zu anderen Programmen wechselt: Auch diese können auf einmal erheblich schlechter reagieren. Dieser Leistungsabfall tritt auf, wenn Code und Daten, die von aktiven Anwendungen im Speicher abgelegt wurden, von speicherintensiven Anwendungen überschrieben werden. Da die anderen Anwendungen ihre Daten und ihren Code anschließend erst wieder von der Festplatte anfordern müssen, laufen sie nur schleppend. Durch die Lösung dieser Probleme mithilfe von SuperFetch haben die Clientversionen von Windows einen großen Schritt nach vorn getan. 542 Kapitel 5 Speicherverwaltung
Komponenten SuperFetch besteht aus mehreren Komponenten im System, die zusammenarbeiten, um den Speicher vorausschauend zu verwalten, und dabei helfen, Beeinträchtigungen des Benutzers durch die Ausführung von SuperFetch zu begrenzen: QQ Ablaufverfolgung Die Mechanismen zur Ablaufverfolgung gehören alle zu der Kernelkomponente Pf, die es SuperFetch ermöglicht, jederzeit ausführliche Informationen über Seitennutzung, Sitzungen und Prozesse abzurufen. Zur Verfolgung der Dateinutzung setzt SuperFetch außerdem den Minifiltertreiber FileInfo ein (%SystemRoot%\System32\­ Drivers\Fileinfo.sys). QQ Sammler und Verarbeiter der Ablaufverfolgung Der Sammler arbeitet mit den Komponenten der Ablaufverfolgung zusammen, um ein Rohprotokoll der erfassten Ablaufverfolgungsdaten bereitzustellen. Diese Daten werden im Arbeitsspeicher gehalten und an den Verarbeiter übergeben, der die Protokolleinträge an die Agenten weitergibt. Diese Agenten wiederum führen die Verlaufsdateien im Arbeitsspeicher und sichern sie auf der Festplatte, wenn der Dienst beendet wird, etwa bei einem Neustart. QQ Agenten SuperFetch führt Informationen über den Dateiseitenzugriff in Verlaufsdateien. Darin sind die virtuellen Offsets vermerkt. Die Agenten gruppieren die Seiten nach Attributen, u. a. den folgenden: • Seitenzugriff, wenn der Benutzer aktiv war • Seitenzugriff durch einen Vordergrundprozess • Harte Fehler, während der Benutzer aktiv war • Seitenzugriffe während des Starts einer Anwendung • Seitenzugriffe, wenn der Benutzer nach einer längeren Leerlaufzeit zurückkehrt QQ Szenario-Manager Diese auch als Kontext-Manager bezeichnete Komponente verwaltet die drei SuperFetch-Szenariopläne Ruhezustand, Standby und schneller Benutzerwechsel. Der Kernelmodusteil des Szenario-Managers stellt APIs bereit, um diese Szenarien auszulösen und zu beenden, um den Status des aktuellen Szenarios zu verwalten und um Ablaufverfolgungsinformationen mit diesen Szenarien zu verknüpfen. QQ Rebalancer Aufgrund der Informationen der SuperFetch-Agenten und des aktuellen Systemstatus (z. B. des Zustands der nach Prioritäten sortierten Seitenlisten) fragt der Rebalancer – ein besonderer Agent im Benutzermodusdienst SuperFetch – die PFN-Datenbank ab und vergibt neue Prioritäten auf der Grundlage des zugehörigen Punktestands jeder Seite. Dadurch stellt er die nach Prioritäten geordnete Standbyliste auf. Der Rebalancer kann auch Befehle an den Speicher-Manager ausgeben, um die Arbeitssätze von Prozessen zu bearbeiten. Er ist der einzige Agent, der tatsächlich Maßnahmen auf dem System ergreifen kann. Die anderen Agenten filtern lediglich die Informationen, die der Rebalancer für seine Entscheidungen heranzieht. Des Weiteren löst der Rebalancer das Prefetching durch den Prefetcherthread aus, der wiederum FileInfo und Kerneldienste nutzt, um den Speicher vorab mit nützlichen Seiten zu laden. Vorausschauende Speicherverwaltung (SuperFetch) Kapitel 5 543
Alle diese Komponenten nutzen Einrichtungen im Speicher-Manager, mit denen sich ausführliche Informationen über den Status der einzelnen Seiten in der PFN-Datenbank, die aktuellen Seitenzähler für die einzelnen Seitenlisten und Prioritätslisten usw. abrufen lassen. Um die Auswirkungen auf den Benutzer zu minimieren, nutzen diese Komponenten außerdem E/A-­ Prioritäten (siehe Kapitel 8 in Band 2). Abb. 5–50 zeigt einen Überblick über die Komponenten von SuperFetch. PFN-Datenbank Rebalancer Benutzerdaten Benutzerdaten Rebalancer-­Kern Kern Agent Kern Agent Kern Agent Benutzerdaten Benutzerdaten Arbeits­satz­ komplement Protokoll­ einträge Verarbeiter der Ablaufverfolgung SeitenzugriffsAblaufverfolgungen Speicher­ priorisierer PFN-Abfrage­ anforderungen PFN-Festlegungs­ anforderungen Speicher-Manager Prefetch­ anforderungen Seiten­zugriffs­ puffer Datenbank mit Abschnittsinformationen Datei­ schlüsselLookup Prefetch­ anforderungen Benutzermodus Vollständige Ablaufver­ folgungen für Seitenzugriff und Dateiinfo PFN-Repriorisierer PFN-Abfrage-/Festlegungs­ anforderungen Sammler der Ablaufverfolgung Prefetch­thread SuperFetchPrefetcher Informationen über Dateinamen/Dateischlüssel SuperFetch-Ablaufverfolgung Kernelmodus Seiten­ zugriffsAblauf­ verfolgung Seiten­infoAblauf­verf. Ablaufverfolgungsliste FileInfo-Minifilter Abbildung 5–50 Architektur von SuperFetch Ablaufverfolgung und Protokollierung SuperFetch trifft die meisten seiner Entscheidungen auf der Grundlage der Informationen, die aus rohen Ablaufverfolgungen und Protokollen übernommen, analysiert und verarbeitet wurden. Dadurch werden diese beiden Komponenten zu den wichtigsten. Die Ablaufverfolgung ähnelt in gewisser Weise ETW, da sie im gesamten System bestimmte Trigger im Code verwendet, um Ereignisse auszulösen. Sie arbeitet aber auch mit anderen vom System bereitgestellten Einrichtungen zusammen, z. B. den Benachrichtigungen des Energie-Managers, Prozesscallbacks und der Dateisystemfilterung. Außerdem verwendet die Ablaufverfolgung sowohl herkömmliche Seitenalterungsmechanismen des Speicher-Managers als auch die jüngere Arbeitssatzalterung und die Zugriffsverfolgung, die für SuperFetch eingerichtet wurden. SuperFetch führt immer eine Ablaufverfolgung aus und fragt kontinuierlich Ablaufverfolgungsdaten vom System ab. Dazu werden über die Zugriffsbitverfolgung und Arbeitssatzalterung des Speicher-Managers Informationen über die Seitennutzung und den Seitenzugriff bestimmt. Genauso wichtig wie die Seitennutzung sind aber auch Informationen über die Dateien, da dies eine Priorisierung der Dateidaten im Cache ermöglicht. Dazu wendet SuperFetch vorhandene Filtermöglichkeiten sowie zusätzlich den FileInfo-Treiber an (siehe Ka- 544 Kapitel 5 Speicherverwaltung
pitel 6). Dieser Treiber befindet sich oberhalb des Dateisystem-Gerätestacks und überwacht den Zugriff auf Dateien und Änderungen daran auf Streamebene, was detaillierte Kenntnisse über den Dateizugriff ermöglicht. (Weitere Informationen über NTFS-Datenstreams erhalten Sie in Kapitel 13 von Band 2.) Die Hauptaufgabe des FileInfo-Treibers besteht darin, die Streams, die nur mit einem eindeutigen, zurzeit im FsContext-Feld des zugehörigen Dateiobjekts implementierten Schlüssel bezeichnet werden, mit Dateinamen zu verknüpfen, sodass der Benutzermodusdienst SuperFetch die jeweiligen Dateistreams und Offsets erkennen kann, zu denen eine Seite auf der Standbyliste eines im Speicher zugeordneten Abschnitts gehört. Außerdem bietet der Treiber eine Schnittstelle, um die Dateidaten unsichtbar vorab abzurufen, ohne gesperrte Dateien und andere Dateisystemzustände zu stören. Des Weiteren sorgt der Treiber dafür, dass die Informationen konsistent bleiben, indem er Löschvorgänge, Umbenennungen, Kürzungen und die Wiederverwendung von Dateischlüsseln mithilfe von Sequenznummern verfolgt. Während einer Ablaufverfolgung kann der Rebalancer jederzeit aufgerufen werden, um die Seiten neu zu füllen. Die Entscheidung dafür erfolgt aufgrund der Analyse von Informationen wie der Verteilung des Speichers auf Arbeitssätze, die Nullseitenliste, die Liste geänderter Seiten und die Standbyliste, der Anzahl von Fehlern, des Status der PTE-Zugriffsbits, der seitenweisen Nutzungsdaten, des aktuellen Verbrauchs an virtuellen Adressen und der Arbeitssatzgrößen. Es gibt mehrere Arten von Ablaufverfolgungen. Eine Seitenzugriffs-Ablaufverfolgung greift auf das Zugriffsbit zurück, um sich zu merken, auf welche Seiten der Prozess zugegriffen hat (sowohl auf Dateiseiten als auch auf privaten Speicher). Eine Ablaufverfolgung zur Namensprotokollierung beobachtet Änderungen an der Zuordnung von Dateinamen zu Dateischlüsseln für die tatsächliche Datei auf der Festplatte. Dadurch kann SuperFetch eine Seite zuordnen, die mit einem Dateiobjekt verknüpft ist. Eine SuperFetch-Ablaufverfolgung merkt sich nur Seitenzugriffe, doch der Dienst SuperFetch verarbeitet diese Ablaufverfolgung im Benutzermodus und geht viel tiefer, wobei er seine eigenen, umfassenderen Informationen hinzufügt, z. B. von wo die Seite geladen wurde (z. B. als residente Datei im Arbeitsspeicher oder aufgrund eines harten Seitenfehlers), ob dies der erste Zugriff auf die Seite war und wie hoch die Seitenzugriffsrate ist. Dazu kommen noch weitere Informationen wie der Systemstatus oder Informationen über nicht lang zurückliegende Szenarien, in denen jeweils zuletzt auf die beobachteten Seiten verwiesen wurde. Diese Ablaufverfolgungsinformationen werden durch einen Logger im Arbeitsspeicher gehalten, und zwar in Datenstrukturen, die die Ablaufverfolgungen bezeichnen und ein Paar aus virtueller Adresse und Arbeitssatz (Ablaufverfolgung des Seitenzugriffs) bzw. aus Datei und Offset (Namensprotokollierung) angeben. Dadurch kann sich SuperFetch merken, für welchen virtuellen Adressbereich eines gegebenen Prozesses bzw. für welchen Offsetbereich einer gegebenen Datei es Seitenereignisse gibt. Szenarien Ein Aspekt von SuperFetch, der von seinen Hauptmechanismen für Seitenpriorisierung und Prefetching abweicht (mehr darüber im nächsten Abschnitt), ist die Nutzung von Szenarien. Vorausschauende Speicherverwaltung (SuperFetch) Kapitel 5 545
Dabei handelt es sich um bestimmte Vorgänge auf dem Computer, die SuperFetch versucht, möglichst angenehm für den Benutzer zu gestalten: QQ Ruhezustand Hierbei muss sinnvoll entschieden werden, welche Seiten außer den vorhandenen Arbeitssatzseiten in der Ruhezustandsdatei gespeichert werden sollen. Damit soll die Zeit, die das System braucht, um nach der Wiederaufnahme des Betriebs wieder reagieren zu können, möglichst kurz gehalten werden. QQ Standby Hierbei sollen harte Fehler nach der Wiederaufnahme völlig vermieden werden. Da ein typisches System den Betrieb in weniger als zwei Sekunden wieder aufnehmen kann, aber unter Umständen bis zu fünf Sekunden braucht, um die Festplatte nach einem langen Schlaf wieder in Marsch zu setzen, kann schon ein einziger harter Seitenfehler eine Verzögerung in dieser Größenordnung bewirken. Um das zu verhindern, priorisiert SuperFetch die nach einem Standby erforderlichen Seiten. QQ Schneller Benutzerwechsel Hierbei müssen die Prioritäten und der Überblick über den Arbeitsspeicher der einzelnen Benutzer genau festgehalten werden. Dadurch kann die Sitzung des neuen Benutzers nach einem Wechsel sofort verwendet werden, ohne dass erst viel Zeit vergeht, um Seiten wieder einzulagern. Jedes dieser Szenarien dient einem eigenen Zweck, aber allgemein geht es darum, das Auftreten harter Seitenfehler zu reduzieren oder ganz auszuschließen. Die Szenarien sind hartcodiert. SuperFetch verwaltet sie durch die APIs NtSetSystemInformation und NtQuerySystemInformation, die den Systemstatus steuern. Für die Zwecke von SuperFetch wird die besondere Informationsklasse SystemSuperFetchInformation verwendet, um die Kernelmoduskomponenten zu steuern und Anforderungen zum Starten, Beenden und Abfragen eines Szenarios oder zur Verknüpfung von Ablaufverfolgungen mit einem Szenario zu stellen. Jedes Szenario wird durch eine Plandatei definiert, die mindestens eine Liste der mit dem Szenario verknüpften Seiten enthält. Es werden auch Seitenprioritätswerte nach bestimmten Regeln zugewiesen (siehe den nächsten Abschnitt). Wenn ein Szenario beginnt, muss der Szenario-Manager auf dieses Ereignis reagieren, indem er eine Liste der Seiten aufstellt, die in den Speicher gebracht werden sollen, und der Prioritäten, mit denen das geschehen soll. Seitenpriorität und Rebalancing Sie haben bereits gesehen, dass der Speicher-Manager ein System von Seitenprioritäten implementiert, um zu definieren, welche Seiten von der Standbyliste für eine gegebene Operation umfunktioniert und in welche Listen die einzelnen Seiten jeweils aufgenommen werden. Dass die Prozesse und Threads dadurch über Prioritäten verfügen, ist von Vorteil. Beispielsweise wird dadurch garantiert, dass ein Defragmentierungsprozess die Standbyliste nicht verunreinigt und keine Seiten von interaktiven Vordergrundprozessen stiehlt. Der wirkliche Nutzen dieses Mechanismus zeigt sich aber erst bei den Seitenpriorisierungsverfahren und dem Rebalancing von SuperFetch, für das keine manuellen Anwendungseingaben und hartcodierten Angaben über die Wichtigkeit der Prozesse erforderlich sind. 546 Kapitel 5 Speicherverwaltung
SuperFetch weist Seitenprioritäten auf der Grundlage eines internen Punktestands der einzelnen Seiten zu, der wiederum teilweise auf der Häufigkeit der Nutzung basiert. Dabei wird gezählt, wie oft eine Seite in einem Zeitintervall verwendet wurde, z. B. in einer Stunde, einem Tag oder einer Woche. Das System merkt sich auch den Zeitpunkt der Verwendung und zeichnet auf, wie lange der letzte Zugriff auf eine Seite zurückliegt. Auch Daten wie der Ursprung der Seite (also die Liste, von der sie stammt) und andere Zugriffsmuster werden zur Berechnung dieses Punktestands herangezogen. Dieser Punktestand wird in eine Prioritätszahl von 1 bis 6 übersetzt. (Für einen anderen Zweck wird eine Priorität von 7 verwendet; mehr darüber später.) Die Seiten auf der Standbyliste mit den niedrigeren Prioritäten werden zuerst wiederverwertet, wie Sie in dem Experiment »Nach Prioritäten geordnete Standbylisten einsehen« schon gesehen haben. Priorität 5 wird gewöhnlich für normale Anwendungen verwendet, Priorität 1 für Hintergrundanwendungen, die Drittentwickler entsprechend kennzeichnen können. Priorität 6 dient dazu, einige sehr wichtige Seiten so spät wie möglich wiederzuverwerten. Die anderen Prioritäten kommen durch die Punktestände der einzelnen Seiten zustande. Da der SuperFetch-Mechanismus das System des Benutzers nach und nach kennenlernt, kann er ohne jegliche Verlaufsdaten bei null beginnen und langsam ein Verständnis der Seitenzugriffe durch den Benutzer aufbauen. Das allerdings würde einen erheblichen Lernaufwand erfordern, wenn eine neue Anwendung, ein neuer Benutzer oder ein neues Servicepack hinzukommt. Stattdessen hat Microsoft SuperFetch im Voraus trainiert. Dazu hat das SuperFetch-Team gängige Verwendungsweisen und Muster erfasst, die bei allen Benutzern auftreten, z. B. Klicken auf das Startmenü, Öffnen der Systemsteuerung oder die Verwendung der Dialogfelder zum Öffnen und Speichern von Dateien, und diese Daten in Ablaufverfolgungen umgewandelt. Diese vorgefertigten Ablaufverfolgungen wiederum wurden in Verlaufsdateien gespeichert, die als Ressourcen in Sysmain.dll enthalten sind. Sie dienen dazu, die besondere Liste für die Priorität 7 zu füllen. Seiten mit dieser Priorität sind Dateiseiten, die auch nach der Beendigung eines Prozesses und sogar über einen Neustart hinweg im Speicher gehalten werden. (Letzteres bedeutet, dass die Seiten nach dem Neustart wieder in den Speicher geladen werden.) Außerdem sind Seiten der Priorität 7 statisch. Das heißt, dass ihre Priorität nie geändert wird und dass SuperFetch niemals andere Seiten außer denen in dem vorkonfigurierten Satz dynamisch mit Priorität 7 lädt. Die nach Prioritäten geordnete Liste wird vom Rebalancer in den Speicher geladen (oder vorab gefüllt), aber um das eigentliche Rebalancing kümmern sich SuperFetch und der Speicher-Manager. Wie Sie gesehen haben, erfolgt die Priorisierung der Standbyliste durch einen internen Mechanismus des Speicher-Managers, wobei die Entscheidungen, welche Seiten als Erste herausgenommen und welche geschützt werden sollen, aufgrund der Prioritätszahl erfolgen. Der Rebalancer verteilt den Speicher nicht neu, sondern ändert die Prioritäten, was den Speicher-Manager veranlasst, die erforderlichen Aufgaben auszuführen. Außerdem ist der Rebalancer dafür zuständig, die Seiten bei Bedarf von der Festplatte zu lesen, sodass sie im Speicher vorhanden sind (Prefetching). Anschließend weist er ihnen die Priorität zu, die von den Agenten für die Punktestände der einzelnen Seiten zugewiesen wurden. Der Speicher-Manager sorgt dafür, dass die Seite ihrer Wichtigkeit entsprechend behandelt wird. Der Rebalancer kann auch aktiv werden, ohne auf andere Agenten zurückzugreifen, etwa wenn er feststellt, dass die Verteilung der Seiten über die Auslagerungsdateien nicht optimal Vorausschauende Speicherverwaltung (SuperFetch) Kapitel 5 547
ist oder dass die Anzahl der wiederverwerteten Seiten auf den verschiedenen Prioritätsebenen nachteilig ist. Außerdem kann der Rebalancer die Verkleinerung von Arbeitssätzen auslösen, die erforderlich sein kann, um ein ausreichendes Budget an Seiten für die von SuperFetch vorab gefüllten Cachedaten zu erstellen. Gewöhnlich nimmt der Rebalancer Seiten geringer Nützlichkeit – z. B. solche, die bereits eine niedrige Priorität haben, die mit Nullen überschrieben sind oder die zwar über gültige Inhalte verfügen, aber nicht zu einem Arbeitssatz gehören und nicht verwendet wurden – und baut daraus einen nützlichen Satz von Seiten im Arbeitsspeicher auf, wobei er auf das Budget zurückgreift, das er sich selbst zugewiesen hat. Nachdem der Rebalancer entschieden hat, welche Seiten er mit welcher Priorität in den Speicher laden muss und welche er daraus entfernt, führt er die erforderlichen Lesevorgänge auf der Festplatte durch, um die Seiten vorab zu laden. Dabei arbeitet er mit dem Priorisierungsverfahren des E/A-­ Managers zusammen, damit seine Ein- und Ausgaben mit niedriger Priorität erfolgen und die Arbeit des Benutzers nicht stören. Für den Speicher, der für das Prefetching erforderlich ist, wird auf Standbyseiten zurückgegriffen. Wie bereits erwähnt, ist dies weiterhin verfügbarer Speicher, da er jederzeit von einem anderen Allozierer als freier Speicher wiederverwertet werden kann. Sollte SuperFetch beim Prefetching einmal die falschen Daten abrufen, so wird der Benutzer dadurch nicht beeinträchtigt, da der Speicher bei Bedarf wiederverwendet werden kann und keine Ressourcen verschlingt. Der Rebalancer wird außerdem in regelmäßigen Abständen ausgeführt, um zu prüfen, ob Seiten mit hoher Priorität tatsächlich kürzlich verwendet wurden. Da solche Seiten selten (wenn überhaupt) wiederverwertet werden, ist es wichtig, sie nicht für Daten zu verschwenden, bei denen nur in einem bestimmten Zeitraum eine häufige Verwendung beobachtet wurde, obwohl in Wirklichkeit nur selten auf sie zugegriffen wird. Wird eine solche Situation erkannt, so wird der Rebalancer erneut ausgeführt, um die betreffenden Seiten auf der Prioritätsliste nach unten zu setzen. An verschiedenen Prefetchingmechanismen ist der Anwendungsstart-Agent beteiligt. Er versucht, den Start von Anwendungen vorauszusehen, und stellt ein Markow-Kettenmodell auf, um aufgrund von gegebenen Anwendungsstarts in einem Zeitabschnitt die Wahrscheinlichkeit für den Start bestimmter Anwendungen zu bestimmen. Als Zeitabschnitte werden dabei vier Zeiträume von durchschnittlich sechs Stunden pro Tag verwendet – morgens, mittags, abends und nachts –, wobei zusätzlich nach Wochenenden und regulären Arbeitstagen unterschieden wird. Wenn ein Benutzer beispielsweise am Samstag- und Sonntagabend erst Word und danach Outlook startet, wird der Anwendungsstart-Agent an Wochenendabenden nach einem Start von Word schon einmal Outlook vorab abrufen. Angesichts der ausreichend großen Mengen von Arbeitsspeicher, die moderne Computer aufweisen – durchschnittlich mehr als 2 GB –, fällt der Betrag, der benötigt wird, um häufig verwendete Prozesse aus Leistungsgründen im Speicher zu halten, kaum ins Gewicht. Häufig kann SuperFetch sämtliche erforderlichen Seiten im RAM unterbringen (wobei SuperFetch allerdings auch auf Systemen mit wenig Speicher funktioniert). Ist das nicht möglich, können Technologien wie ReadyBoost und ReadyDrive dabei helfen, die Nutzung der Festplatte zu verringern. 548 Kapitel 5 Speicherverwaltung
Leistungsstabilisierung SuperFetch verfügt außerdem über eine Leistungsstabilisierung. Diese Komponente wird vom Benutzermodusdienst SuperFetch verwaltet, ist aber letzten Endes im Kernel implementiert, nämlich in den Pf-Routinen. Sie sucht nach Dateizugriffsvorgängen, die die Systemleistung beeinträchtigen, indem sie Standbylisten mit nicht benötigten Daten füllt. Wenn beispielsweise ein Prozess eine große Datei im Dateisystem kopiert, würde die Standbyliste mit dem Inhalt dieser Datei gefüllt, auch wenn nie wieder oder zumindest lange Zeit kein Zugriff mehr auf diese Datei erfolgt. Dadurch würden andere Dateien mit der gleichen Priorität aus dem Arbeitsspeicher entfernt, und wenn die Datei zu einem interaktiven und nützlichen Programm gehört, ist es sehr wahrscheinlich, dass diese Priorität mindestens 5 beträgt. SuperFetch reagiert auf zwei bestimmte Arten von E/A-Zugriffsmustern: QQ Sequenzieller Dateizugriff Hierbei durchläuft das System alle Daten in einer Datei. QQ Sequenzieller Verzeichniszugriff Hierbei durchläuft das System alle Dateien in einem Verzeichnis. Wenn die Menge an Daten, die aufgrund eines Zugriffs einer dieser Arten in die Standbyliste aufgenommen wurden, einen internen Schwellenwert überschreitet, wendet SuperFetch eine aggressive Prioritätssenkung auf die zur Zuordnung der Datei verwendeten Seiten an. Dies geschieht nur im Zielprozess, damit andere Anwendungen nicht bestraft werden. Die betroffenen Seiten erhalten die neue Priorität 2. Da diese Komponente von SuperFetch reaktiv und nicht vorausschauend arbeitet, dauert es einige Zeit, bis sie Wirkung zeigt. SuperFetch behält diesen Prozess daher im Auge. Wenn der Prozess immer diese Art von sequenziellem Zugriff durchführt, so kann sich SuperFetch daran erinnern und senkt die Prioritäten der Dateiseiten schon, sobald sie zugeordnet werden, anstatt erst nachträglich zu reagieren. Der gesamte Prozess unterliegt jetzt bereits einer Leistungsstabilisierung für zukünftige Dateizugriffe. Dadurch aber könnte SuperFetch legitime Anwendungen beeinträchtigen, die sequenzielle Zugriffe durchführen. Beispielsweise können Sie mit dem Sysinternals-Tool Strings.exe nach Strings in allen ausführbaren Dateien in einem Verzeichnis suchen. Wenn es viele solcher Dateien gibt, wird SuperFetch wahrscheinlich eine Leistungsstabilisierung durchführen. Wenn Sie Strings.exe das nächste Mal mit einem anderen Suchparameter ausführen, würde das Programm dann langsamer ausgeführt werden als beim ersten Mal. Um das zu verhindern, unterhält SuperFetch neben der Liste der Prozesse, die beobachtet werden müssen, auch eine interne, hartcodierte Liste von Ausnahmen. Greift ein Prozess später erneut auf leistungsstabilisierte Dateien zu, wird die Leistungsstabilisierung für ihn ausgeschaltet, um das erwartete Verhalten wiederherzustellen. Ein wichtiger Punkt der Leistungsstabilisierung – und von SuperFetch-Optimierungen im Allgemeinen – ist die Tatsache, dass SuperFetch die Nutzungsmuster ständig beobachtet und sein Verständnis des Systems ständig aktualisiert, um einen Abruf unnützer Daten zu vermeiden. Zwar können Änderungen in der Tagesroutine des Benutzers oder im Startverhalten von Anwendungen dazu führen, dass SuperFetch den Cache mit irrelevanten Daten füllt oder vermeintlich nutzlose Daten entfernt, aber der Mechanismus kann sich rasch an solche geänderten Muster anpassen. Wenn die Aktionen des Benutzers erratisch sind, verhält sich das System Vorausschauende Speicherverwaltung (SuperFetch) Kapitel 5 549
schlimmstenfalls so, als wäre SuperFetch gar nicht vorhanden. Im Zweifelfall oder wenn keine zuverlässigen Daten erfasst werden können, hält SuperFetch sich zurück und nimmt keine Änderungen an dem betreffenden Prozess oder der Seite vor. ReadyBoost Im Vergleich zu der Situation vor einem Jahrzehnt ist RAM heutzutage gut verfügbar und relativ günstig, aber noch nicht billig genug, um die Kosten von Sekundarspeicher wie Festplatten zu schlagen. Leider enthalten mechanische Festplatten viele bewegliche Teile, sind störanfällig und im Vergleich zum RAM langsam, insbesondere bei Suchvorgängen. Daher ist die Speicherung aktiver SuperFetch-Daten auf der Festplatte praktisch genauso schlecht wie die Auslagerung einer Seite, die dann erst wieder über einen harten Seitenfehler in den Speicher zurückgeholt werden muss. Bei SSD- und Hybridfestplatten sind diese Nachteile weniger stark ausgeprägt, allerdings sind die Geräte immer noch teurer und langsamer als RAM. SSD-Wechselmedien wie USBSticks, CompactFlash- und Secure-Digital-Karten stellen einen brauchbaren Kompromiss dar. Sie sind billiger als RAM und mit größerem Speichervolumen erhältlich und weisen gleichzeitig aufgrund ihres Mangels an beweglichen Teilen kürzere Suchzeiten auf als mechanische Festplatten. In der Praxis werden CompactFlash- und Secure-Digital-Karten fast immer über einen USB-Adapter angeschlossen, sodass sie im System als USB-Medien erscheinen. Wahlfreie Festplatten-E/A ist besonders langsam, da sich die Suchzeit und die Rotationslatenz eines typischen Desktop-Festplattenlaufwerks auf insgesamt etwa 10 ms belaufen kann, was für die heutigen Prozessoren mit 3 oder gar 4 GHz eine Ewigkeit darstellt. Flash-Speicher dagegen kann wahlfreie Lesevorgänge bis zu zehnmal schneller erledigen als eine typische Festplatte. Daher bietet Windows die ReadyBoot-Funktion, um die Vorteile von Flash-Speichergeräten für den Arbeitsspeicher nutzen zu können. Dabei wird eine Cachingschicht aufgebaut, die sich logisch zwischen dem Arbeitsspeicher und dem Flash-Medium befindet. ReadyBoost (nicht zu verwechseln mit ReadyBoot!) wird mithilfe eines Treibers implementiert (%SystemRoot&\System32\Drivers\Rdyboost.sys), der die zwischengespeicherten Daten in das NVRAM-Gerät (Non-Volatile RAM, »nicht flüchtiger RAM«) schreibt. Wenn Sie einen USBStick an ein System anschließen, schaut sich ReadyBoost die Leistungseigenschaften des Geräts an und speichert den Befund in HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Emdmgmt. Emd steht hier für »External Memory Drive«, was die Bezeichnung für ReadyBoost während der Entwicklung war. Wenn das neue Gerät zwischen 256 MB und 32 GB groß ist und über eine Übertragungsrate von mindestens 2,5 MB pro Sekunde für 4-KB-Lesevorgänge und von mindestens 1,75 MB für wahlfreie 512-KB-Schreibvorgänge verfügt, fragt ReadyBoost, ob Sie einen Teil des Platzes für die Zwischenspeicherung auf der Festplatte zur Verfügung stellen möchten. Wenn Sie zustimmen, erstellt ReadyBoost im Stamm des Geräts die Datei ReadyBoost.sfcache und verwendet diese, um zwischengespeicherte Seiten abzulegen. 550 Kapitel 5 Speicherverwaltung
Nach der Initialisierung der Zwischenspeicherung fängt ReadyBoost alle Lese- und Schreibvorgänge auf lokale Festplattenvolumes (z. B. C:\) ab und kopiert alle gelesenen oder geschriebenen Daten in die von dem Dienst erstellte Cachingdatei. Ausgenommen davon sind u. a. Daten, die lange nicht gelesen wurden oder die zu Volume-Snapshot-Anforderungen gehören. Die auf dem Cachelaufwerk gespeicherten Daten werden komprimiert, wobei gewöhnlich ein Verhältnis von 2:1 erreicht wird, sodass eine 4-GB-Cachedatei 8 GB an Daten fassen kann. Jeder Block wird beim Schreiben mit AES verschlüsselt, wobei ein für jeden Systemstart neu erstellter, zufälliger Sitzungsschlüssel verwendet wird, um die Daten in dem Cache für den Fall zu schützen, dass das Gerät von dem Computer entfernt wird. Wenn wahlfreie Lesevorgänge im Cache erledigt werden können, nutzt ReadyBoost dafür tatsächlich die Cachedatei. Da der sequenzielle Lesezugriff auf Festplatten jedoch günstiger ist als im Flash-Speicher, werden Lesevorgänge, die zu einem sequenziellen Zugriff gehören, weiterhin direkt auf der Festplatte erledigt, auch wenn sich die entsprechenden Daten im Cache befinden. Das Gleiche gilt auch für Lesevorgänge im Rahmen umfangreicher Ein-/Ausgaben. Ein Nachteil der Verwendung von Flashmedien besteht darin, dass der Benutzer sie jederzeit vom Computer entfernen kann, weshalb das System kritische Daten nicht ausschließlich dort speichern darf. (Wie Sie gesehen haben, werden Schreibvorgänge immer erst im Sekundärspeicher durchgeführt.) Die verwandte ReadyDrive-Technologie, die wir im nächsten Abschnitt erläutern, bietet zusätzliche Vorteile und löst dieses Problem. ReadyDrive ReadyDrive ist eine Windows-Technologie, die die Vorteile von Hybridfestplatten (Hybrid Hard Disk Drives, H-HDD) nutzt. Dabei handelt es sich um eine Festplatte mit eingebettetem NVRAM. Gewöhnlich enthalten H-HDDs einen Cache zwischen 50 und 512 MB. Bei der Verwendung von ReadyDrive dient der Flashspeicher des Laufwerks nicht einfach als automatischer, transparenter Cache wie bei den meisten Festplatten. Stattdessen legt Windows mithilfe von ATA-8-Befehlen fest, welche Daten auf der Festplatte im Flashspeicher festgehalten werden sollen. Beispielsweise speichert Windows Bootdaten im Cache, wenn das System heruntergefahren wird, was einen schnelleren Neustart ermöglicht. Ebenso werden einige Daten der Ruhezustandsdatei im Cache abgelegt, sodass das Aufwachen aus dem Ruhezustand schneller erfolgt. Da der Cache auch bei abgeschalteter Festplatte aktiviert ist, kann Windows den Flashspeicher als Schreibcache verwenden und damit ein Einschalten der Festplatte bei niedrigem Akkustand vermeiden. Dadurch lässt sich bei normalem Gebrauch sehr viel Strom sparen. ReadyDrive wird auch von SuperFetch genutzt, da es die gleichen Vorteile wie ReadyBoost, zusätzlich aber weitere Möglichkeiten, bietet. Beispielsweise ist kein externes Flashlaufwerk erforderlich. Außerdem kann SuperFetch den Cache dauerhaft nutzen, denn da er sich auf der physischen Festplatte befindet, wird er gewöhnlich nicht im laufenden Betrieb entfernt. Der Festplattencontroller muss sich daher keine Sorgen machen, dass die Daten verschwinden, und kann Schreibvorgänge auf der eigentlichen Festplatte vermeiden, indem er ausschließlich den Cache benutzt. Vorausschauende Speicherverwaltung (SuperFetch) Kapitel 5 551
Prozessreflexion Es kann vorkommen, dass sich ein Prozess problematisch verhält, aber immer noch einen Zweck erfüllt, weshalb es nicht angebracht ist, ihn anzuhalten, um einen kompletten Speicherauszug zu erstellen oder ihn interaktiv zu debuggen. Der Zeitraum, in dem der Prozess angehalten wird, um einen Speicherauszug zu erstellen, lässt sich zwar verkleinern, indem man nur einen Miniauszug aufnimmt – bei dem nur die Threadregister und Stacks sowie die Seiten erfasst werden, auf die die Register verweisen –, doch ein solcher Auszug bietet nur sehr eingeschränkte Informationen. Zur Diagnose von Abstürzen mögen sie oft ausreichen, aber nicht für die Störungssuche im Allgemeinen. Bei der Prozessreflexion wird der Zielprozess nur so lange angehalten, um einen Miniauszug sowie einen angehaltenen Klon von ihm zu erstellen. Während der Zielprozess danach weiter ausgeführt wird, kann aus dem Klon ein umfassender Auszug erstellt werden, der den gesamten gültigen Benutzermodusspeicher des Prozesses umfasst. Mehrere WDI-Komponenten (Windows Diagnostic Infrastructure) nutzen die Prozessreflexion, um auf diese minimalinvasive Weise Speicherauszüge der Prozesse zu erstellen, die von ihren Heuristiken als verdächtig eingestuft werden. Beispielsweise legt der Memory Leak Diagnoser von Windows Resource Exhaustion Detection and Resolution (RADAR genannt) auf diese Weise einen Speicherauszug von Prozessen an, bei denen Lecks im privaten virtuellen Arbeitsspeicher vermutet werden. Dieser Speicherauszug kann dann über die Windows-Fehlerberichterstattung zur Analyse an Microsoft gesendet werden. Die WDI-Heuristiken für hängende Prozesse gehen bei Prozessen, zwischen denen Deadlocks zu bestehen scheinen, auf die gleiche Weise vor. Da diese Komponenten Heuristiken verwenden, können sie nicht sicher sein, dass mit den Prozessen tatsächlich etwas nicht stimmt, weshalb sie sie nicht einfach für längere Zeit anhalten oder gar beenden dürfen. Die Funktion RtlCreateProcessReflection in Ntdll.dll sorgt für die Prozessreflexion: 1. Sie erstellt einen gemeinsam genutzten Speicherabschnitt. 2. Sie füllt diesen gemeinsam genutzten Speicherabschnitt mit Parametern. 3. Sie ordnet den gemeinsam genutzten Speicherabschnitt dem aktuellen und dem Zielprozess zu. 4. Sie erstellt zwei Ereignisobjekte und kopiert sie in den Zielprozess, sodass der aktuelle und der Zielprozess ihre Operationen synchronisieren können. 5. Mit einem Aufruf von RtlpCreateUserThreadEx injiziert sie einen Thread in den Zielprozess. Mit der Ntdll-Funktion RtlpProcessReflectionStartup wird dieser Thread angewiesen, mit der Ausführung zu beginnen. Da Ntdll.dll in den Adressräumen aller Prozessen an der gleichen zufällig beim Start zugeordneten Adresse zugeordnet ist, kann der aktuelle Prozess einfach die Adresse der Funktion aus seiner eigenen Ntdll.dll-Zuordnung übergeben. 6. Wenn der Aufrufer von RtlCreateProcessReflection ein Handle für den geklonten Prozess verlangt hat, wartet die Funktion darauf, dass der Remotethread beendet wird. Anderenfalls gibt sie die Steuerung an den Aufrufer zurück. 7. Der injizierte Thread im Zielprozess weist ein weiteres Ereignisobjekt für die Synchronisierung mit dem geklonten Prozess zu. 552 Kapitel 5 Speicherverwaltung
8. Der injizierte Thread ruft RtlCloneUserProcess auf und übergibt die Parameter aus der Speicherzuordnung, die er sich mit dem auslösenden Prozess teilt. 9. Es gibt eine Option für RtlCreateProcessReflection, um zu verlangen, dass der Prozess nur dann geklont wird, wenn er nicht im Lader ausgeführt wird, Heapoperationen durchführt, den Prozessumgebungsblock (PEB) oder lokalen Fiberspeicher bearbeitet. Wenn diese Option vorhanden ist, erwirbt RtlCreateProcessReflection zunächst die entsprechenden Sperren. Das kann für das Debugging nützlich sein, da sich die Kopie der Datenstrukturen im Speicherauszug dann in einem konsistenten Zustand befindet. 10. Abschließend ruft RtlCloneUserProcess die Benutzermodusfunktion RtlCreateUserProcess auf, die für die Prozesserstellung im Allgemeinen zuständig ist, und übergibt dabei Flags, um festzulegen, dass der neue Prozess ein Klon des aktuellen sein soll. RtlpCreateUserProcess wiederum ruft ZwCreateUserProcess auf, um den Kernel zu bitten, den Prozess zu erstellen. Beim Klonen eines Prozesses führt ZwCreateUserProcess zum Großteil denselben Code aus wie beim Erstellen eines neuen Prozesses. Die Ausnahme besteht in der Funktion PspAllocateProcess, die sie aufruft, um das Prozessobjekt und den ursprünglichen Thread anzulegen: Diese Funktion ruft ihrerseits MmInitializeProcessAddressSpace mit einem Flag auf, durch das als Adresse eine Copy-on-Write-Kopie des Zielprozesses verwendet wird und nicht ein neuer Prozessadressraum. Der Speicher-Manager nutzt dieselbe Unterstützung, die er für die API fork für Services for Unix Application bereitstellt, um den Adressraum effizient zu klonen. Sobald der Zielprozess die Ausführung wieder aufnimmt, werden alle Änderungen, die er an seinem Adressraum vornimmt, nur noch von ihm selbst gesehen, nicht aber von dem Klon. Der Adressraum des Klons stellt daher eine Momentaufnahme des Zielprozesses dar. Die Ausführung des Klons beginnt, sobald RtlCreateUserProcess die Steuerung zurückgegeben hat. Wurde der Klon erfolgreich erstellt, empfängt sein Thread den Rückgabecode STATUS_PROCESS_CLONED und der klonende Thread STATUS_SUCCESS. Der geklonte Prozess synchronisiert sich nun mit dem Zielprozess und ruft schließlich eine Funktion auf, die optional an RtlCreateProcessReflection übergeben werden kann. Dabei muss es sich um eine in Ntdll. dll implementierte Funktion handeln. Beispielsweise gibt RADAR hier RtlDetectHeapLeaks an, die eine grundlegende heuristische Analyse der Prozessheaps durchführt und das Ergebnis an den Thread meldet, der RtlCreateProcessReflection aufgerufen hat. Wurde keine Funktion angegeben, hält sich der Thread je nach den an RtlCreateProcessReflection übergebenen Flags entweder selbst an oder beendet sich. Um die Prozessreflexion zu nutzen, rufen RADAR und WDI die Funktion RtlCreateProcessReflection auf und weisen sie an, ein Handle für den geklonten Prozess zurückzugeben und dafür zu sorgen, dass sich der Klon nach seiner Initialisierung selbst anhält. Anschließend legen Sie einen Miniauszug des Zielprozesses an, wobei dieser für die Dauer dieses Vorgangs angehalten wird. Danach erstellen sie aus dem geklonten Prozess einen umfassenderen Speicherauszug und beenden den Klon schließlich. Zwischen dem Abschluss des Miniauszugs und dem Erstellen des Klons kann der Zielprozess weiterlaufen, doch in den meisten Fällen beeinträchtigen Inkonsistenzen die Störungssuche nicht. Wenn Sie das Sysinternals-Programm Procdump mit dem Schalter -r aufrufen, geht es auf die gleiche Weise vor, um mithilfe der Selbstreflexion einen Speicherauszug des Zielprozesses anzulegen. Vorausschauende Speicherverwaltung (SuperFetch) Kapitel 5 553
Zusammenfassung In diesem Kapitel haben Sie sich angesehen, wie der Speicher-Manager von Windows den virtuellen Speicher verwaltet. Wie in den meisten modernen Betriebssystemen hat jeder Prozess Zugriff auf einen privaten Adressraum, wodurch der Speicher eines Prozesses geschützt wird, andererseits aber eine gemeinsame Nutzung des Speichers durch Prozesse auf effi­ ziente und sichere Weise möglich ist. Darüber hinaus gibt es noch zusätzliche Möglichkeiten wie die Verwendung zugeordneter Dateien und von dünn zugewiesenem Arbeitsspeicher. Das Windows-Umgebungsteilsystem stellt die meisten Fähigkeiten des Speicher-Managers über die Windows-API für Anwendungen bereit. Im nächsten Kapitel beschäftigen wir uns mit einem weiteren entscheidenden Bestandteil eines jedes Betriebssystems – dem E/A-System. 554 Kapitel 5 Speicherverwaltung
K apite l 6 Das E/A-System Das E/A-System von Windows besteht aus mehreren Exekutivkomponenten, die zusammen die Hardwaregeräte verwalten und den Anwendungen und dem System eine Schnittstelle dazu zur Verfügung stellen. In diesem Kapitel betrachten wir als Erstes die Designziele für das E/A-System, die die Implementierung beeinflusst haben, und dann die Komponenten, aus denen dieses System besteht – den E/A-Manager, den PnP-Manager (Plug & Play) und die Energieverwaltung. Anschließend untersuchen wir die wichtigsten Datenstrukturen zur Beschreibung von Geräten, Gerätetreibern und E/A-Anforderungen und die erforderlichen Schritte, um E/A-Anforderungen zu erfüllen und abzuschließen. Abschließend schauen wir uns die Funktionsweise der Geräteerkennung, der Treiberinstallation und der Energieverwaltung an. Komponenten des E/A-Systems Beim Design des Windows-E/A-Systems bestand das Ziel darin, Anwendungen eine Abstraktion sowohl von Hardware- als auch von Softwaregeräten (physischen und logischen Geräten) mit den folgenden Eigenschaften zur Verfügung zu stellen: QQ Einheitliche Sicherheitsvorkehrungen und Benennung für alle Geräte, um gemeinsam nutzbare Ressourcen zu schützen. (Eine Beschreibung des Windows-Sicherheitsmodells finden Sie in Kapitel 7.) QQ Asynchrone, paketgestützte Hochleistungs-E/A, um die Implementierung skalierbarer Anwendungen zu ermöglichen. QQ Dienste, die es erlauben, Treiber in einer Hochsprache zu schreiben und von einer Maschinenarchitektur zur anderen zu übertragen. QQ Geschichteter Aufbau und Erweiterbarkeit, um Treiber hinzufügen zu können, die das Verhalten der anderen Treiber oder der Geräte transparent verändern, ohne jegliche Änderungen an diesen anderen Treibern oder Geräten vornehmen zu müssen. QQ Dynamisches Laden und Entladen von Gerätetreibern, sodass sie bei Bedarf geladen werden können und keine Systemressourcen verbrauchen, wenn sie nicht benötigt werden. QQ Plug-&-Play-Unterstützung; das heißt, das System kann Treiber für neu erkannte Hardware finden und installieren, die erforderlichen Hardwareressourcen zuweisen und es Anwendungen ermöglichen, Geräteschnittstellen zu entdecken und zu aktivieren. QQ Unterstützung für die Energieverwaltung, sodass das gesamte System oder einzelne Geräte in Energiesparzustände eintreten können. QQ Unterstützung für mehrere installierbare Dateisysteme einschließlich FAT (und die Varianten FAT32 und exFAT), das CD-ROM-Dateisystem (CDFS), Universal Disk Format (UDF), Re- 555
silient File System (ReFS) und das Windows-Dateisystem (NTFS). Genauere Informationen über die einzelnen Arten von Dateisystemen und ihre Architektur erhalten Sie in Kapitel 13 von Band 2. QQ WMI-Unterstützung (Windows Management Instrumentation) und Diagnosefähigkeiten, sodass Treiber mithilfe von WMI-Anwendungen und -Skripten verwaltet und überwacht werden können (siehe Kapitel 9 in Band 2). Um all diese Merkmale bieten zu können, besteht das Windows-E/A-System aus mehreren Exekutivkomponenten und Gerätetreibern (siehe Abb. 6–1). Anwendungen/Dienste WMI-Dienst PnP-Manager des Benutzermodus SetupKomponenten Benutzermodus Kernelmodus WMI-Routinen PnP-Manager Treiber Treiber Energie­ verwaltung Treiber E/A-Manager INF-Dateien, CAT-Dateien, Regis­trierung E/A-System Treiber HAL Abbildung 6–1 Bestandteile des E/A-Systems QQ Der E/A-Manager bildet das Herz des E/A-Systems. Er verbindet Anwendungen und Systemkomponenten mit virtuellen, logischen und physischen Geräten und definiert die In­ frastruktur zur Unterstützung der Gerätetreiber. QQ Ein Gerätetreiber stellt gewöhnlich eine E/A-Schnittstelle für eine bestimmte Art von Gerät zur Verfügung. Ganz allgemein ist ein Treiber ein Softwaremodul, das Befehle hoher Ebene wie Lese- oder Schreibbefehle interpretiert und maschinennahe, gerätespezifische Befehle ausgibt, z. B. das Schreiben in ein Steuerregister. Gerätetreiber empfangen die Befehle für die von ihnen verwalteten Geräte über den E/A-Manager und informieren ihn wiederum darüber, dass sie die Verarbeitung dieser Befehle abgeschlossen haben. Häufig nutzen Gerätetreiber den E/A-Manager auch, um E/A-Befehle an andere Gerätetreiber weiterzuleiten, mit denen sie sich die Schnittstelle oder Steuerimplementierung des Geräts teilen. QQ Der PnP-Manager arbeitet eng mit dem E/A-Manager und den Bustreibern zusammen, um die Zuweisung von Hardwareressourcen zu lenken und um das Hinzufügen und Entfernen von Hardwaregeräten zu erkennen und darauf zu reagieren. Gemeinsam sind der 556 Kapitel 6 Das E/A-System
PnP-Manager und der Bustreiber dafür zuständig, bei der Entdeckung eines Geräts dessen Treiber zu laden. Wird dem System ein Gerät hinzugefügt, für das kein geeigneter Treiber vorhanden ist, ruft die PnP-Komponente der Exekutive die Geräteinstallationsdienste des PnP-Managers im Benutzermodus auf. QQ Auch die Energieverwaltung arbeitet eng mit dem E/A- und dem PnP-Manager zusammen, um das System und die einzelnen Gerätetreiber durch Statusübergänge beim Energieverbrauch zu lotsen. QQ Gerätetreiber können indirekt als Anbieter fungieren, indem sie die WMI-Unterstützungsroutinen des WDM-WMI-Anbieters (Windows Driver Model) als Vermittler für die Kommunikation mit dem WMI-Dienst im Benutzermodus verwenden. QQ Die Registrierung dient als Datenbank, in der eine Beschreibung der grundlegenden an das System angeschlossenen Hardwaregeräte sowie die Initialisierungs- und Konfigura­ tionseinstellungen der Treiber gespeichert werden (siehe den Abschnitt »Die Registrierung« in Kapitel 9 von Band 2). QQ Bei den INF-Dateien handelt es sich um Treiberinstallationsdateien. Sie bilden die Verbindung zwischen einem Hardwaregerät und dem Treiber, der hauptsächlich für seine Steuerung zuständig ist. Diese Dateien bestehen aus skriptartigen Anweisungen, die das zugehörige Gerät beschreiben und die Quell- und Zielspeicherorte der Treiberdateien, die zur Treiberinstallation erforderlichen Registrierungsänderungen sowie treiberspezifische Informationen angeben. Die digitalen Signaturen, mit denen Windows bestätigt, dass eine Treiberdatei die WHQL-Tests (Windows Hardware Quality Labs) bestanden hat, sind in CAT-Dateien gespeichert. Digitale Signaturen dienen auch als Schutz gegen eine Manipulation des Treibers oder seiner INF-Datei. QQ Die Hardwareabstraktionsschicht (HAL) schirmt die Treiber gegen die technischen Einzelheiten des Prozessors und des Interruptcontrollers ab, indem sie APIs bereitstellt, die die Unterschiede zwischen verschiedenen Plattformen zudecken. Im Grunde genommen ist die HAL der Bustreiber für alle auf der Hauptplatine des Computers festgelöteten Geräte, die nicht von anderen Treibern gesteuert werden. Der E/A-Manager Der E/A-Manager ist das Herz des E/A-Systems. Er definiert das Framework oder Modell zur Auslieferung von E/A-Anforderungen an Gerätetreiber. Das E/A-System ist paketgestützt. Die meisten E/A-Anforderungen werden durch ein E/A-Anforderungspaket (I/O Request Packet, IRP) dargestellt. Diese Datenstruktur beschreibt die Anforderung vollständig und bewegt sich zwischen den Komponenten des E/A-Systems. (Wie Sie im Abschnitt »Schnelle E/A« noch sehen werden, bildet die schnelle E/A eine Ausnahme, denn hierbei werden keine IRPs verwendet.) Dadurch kann ein einzelner Anwendungsthread mehrere E/A-Anforderungen gleichzeitig handhaben. Weitere Informationen über IRPs erhalten Sie weiter hinten im Abschnitt »E/A-­ Anforderungspakete«. Der E/A-Manager erstellt im Arbeitsspeicher ein IRP, um eine E/A-Operation darzustellen, und übergibt diesem Paket einen Zeiger auf den passenden Treiber. Wenn ein Treiber ein IRP empfängt, führt er die darin angegebene Operation aus und gibt das Paket dann wieder an Komponenten des E/A-Systems Kapitel 6 557
den E/A-Manager zurück, da entweder die Operation abgeschlossen ist oder eine Weitergabe an einen anderen Treiber für eine zusätzliche Verarbeitung erforderlich ist. Wenn die Operation komplett abgeschlossen ist, verwirft der E/A-Manager das Paket. Daneben stellt der E/A-Manager auch gemeinsamen Code bereit, den die verschiedenen Treiber für ihre E/A-Verarbeitung aufrufen können. Durch die Konsolidierung dieser allgemeinen Aufgaben in den E/A-Manager lassen sich die einzelnen Treiber einfacher und kompakter gestalten. Beispielsweise enthält der E/A-Manager eine Funktion, mit der Treiber andere Treiber aufrufen können. Er verwaltet auch Puffer für E/A-Anforderungen, bietet eine Timeoutunterstützung für Treiber und zeichnet auf, welche installierten Dateisysteme im Betriebssystem geladen sind. Im E/A-Manager gibt es etwa 100 verschiedene Routinen, die von Gerätetreibern aufgerufen werden können. Des Weiteren stellt der E/A-Manager flexible E/A-Dienste bereit, mit denen Umgebungsteilsysteme wie die für Windows und POSIX (wobei Letzteres nicht mehr unterstützt wird) ihre jeweiligen E/A-Funktionen implementieren können. Diese Dienste bieten unter anderem Unterstützung für asynchrone E/A, womit Entwickler skalierbare Hochleistungs-Serveranwendungen erstellen können. Die einheitliche, modulare Schnittstelle der Treiber ermöglicht dem E/A-Manager, jeden Treiber ohne genaue Kenntnisse seines Aufbaus und seiner Interna aufzurufen. Das Betriebssystem behandelt alle E/A-Anforderungen so, als wären sie an eine Datei gerichtet. Der Treiber wandelt diese Anforderungen an eine virtuelle Datei in hardwarespezifische Anforderungen um. Über den E/A-Manager können sich Treiber auch gegenseitig aufrufen, um die E/A-Anforderung in unabhängigen Schichten auszuführen. Neben den regulären Funktionen zum Öffnen, Schließen, Lesen und Schreiben bietet das Windows-E/A-System auch noch fortgeschrittene Möglichkeiten, z. B. für asynchrone, direkte, gepufferte und Scatter-Gather-E/A, die wir alle weiter hinten im Abschnitt »Verschiedene Arten der E/A« genauer beschreiben werden. Typische E/A-Verarbeitung Meistens sind an E/A-Operationen nicht alle Komponenten des E/A-Systems beteiligt. Eine typische E/A-Anforderung beginnt damit, dass eine Anwendung eine E/A-Funktion ausführt, z. B. das Lesen von Daten von einem Gerät, die dann vom E/A-Manager, einem oder mehreren Gerätetreibern und der HAL verarbeitet wird. In Windows führen Threads Ein- und Ausgaben an virtuellen Dateien durch. Das sind jegliche Quellen oder Ziele für die E/A, die wie eine Datei behandelt werden (z. B. Geräte, Dateien, Verzeichnisse, Pipes und Mailslots). Ein typischer Benutzermodusclient ruft die Funktion CreateFile oder CreateFile2 auf, um ein Handle zu einer virtuellen Datei zu erhalten. Diese Funktionsnamen sind jedoch irreführend, denn es geht hier nicht nur um Dateien, sondern um alles, was im Objekt-Manager-Verzeichnis GLOBAL?? als symbolische Verknüpfung bezeichnet wird. Die Bezeichnung File in diesen Funktionsnamen bedeutet nur ein virtuelles Dateiobjekt (FILE_OBJECT), das von der Exekutive als Ergebnis dieser Funktionen erstellt wird. Abb. 6–2 zeigt das Verzeichnis GLOBAL?? in der Anzeige des Sysinternals-Tools WinObj. Wie Sie dort erkennen können, ist ein Name wie C: lediglich eine symbolische Verknüpfung zu einem internen Namen im Objekt-Manager-Verzeichnis Device (in diesem Fall \Device\Hard- 558 Kapitel 6 Das E/A-System
diskVolume7). (Mehr über den Objekt-Manager und seinen Namespace erfahren Sie in Kapitel 8 von Band 2.) Alle Namen im Verzeichnis GLOBAL?? sind mögliche Argumente für CreateFile(2). Kernelmodusclients, z. B. Gerätetreiber, können die ähnliche Funktion ZwCreateFile nutzen, um ein Handle zu einer virtuellen Datei zu bekommen. Abbildung 6–2 Das Verzeichnis GLOBAL?? des Objekt-Managers Abstraktionen höherer Ebene wie das .NET-Framework und die Windows-Runtime bringen ihre eigenen APIs für die Arbeit mit Dateien und Geräten mit (z. B. die Klassen System.IO.File in .NET und Windows.Storage.StorageFile in WinRT). Um das eigentliche Handle zu erhalten, rufen diese APIs allerdings letztendlich CreateFile(2) auf. Für das Verzeichnis GLOBAL?? des Objekt-Managers finden Sie manchmal auch noch die ältere Bezeichnung DosDevices. Dieser Name funktioniert immer noch, da er als symbolische Verknüpfung zu GLOBAL?? im Stamm des Objekt-Manager-Namespaces definiert ist. In Treibercode wird zum Verweis auf GLOBAL?? gewöhnlich der String ?? verwendet. Das Betriebssystem abstrahiert alle E/A-Anforderungen als Operationen auf einer virtuellen Datei, da der E/A-Manager keinerlei Kenntnisse über irgendetwas außer über diese Dateien hat. Daher ist der Treiber dafür verantwortlich, dateiorientierte Befehle (Öffnen, Schließen, Lesen, Schreiben) in gerätespezifische umzuwandeln. Durch diese Abstraktion wird die Schnittstelle einer Anwendung zu den Treibern generalisiert. Benutzermodusanwendungen rufen dokumentierte Funktionen auf, die wiederum interne E/A-Systemfunktionen zum Lesen und Schreiben in einer Datei und für andere Operationen aufrufen. Der E/A-Manager leitet diese Anforderungen für virtuelle Dateien dynamisch an die passenden Gerätetreiber weiter. Abb. 6–3 illustriert den Komponenten des E/A-Systems Kapitel 6 559
grundlegenden Ablauf einer Anforderung für einen Lesevorgang. Andere Arten von E/A-Anforderungen, z. B. zum Schreiben, laufen ähnlich ab; der Unterschied besteht nur in den verwendeten APIs. ReadFile NtReadFile Benutzermodus sysenter / syscall / svc Kernelmodus NtReadFile E/A-Manager (IoCallDriver) Treiber­unter­ stützungs­ routinen (Io, Ke, Ex, Mm, Hal, FsRtl, … Auslösen der E/A HALZugriffsroutinen Gerät Abbildung 6–3 Ablauf einer typischen E/A-Anforderung In den folgenden Abschnitten betrachten wir diese Komponenten genauer. Dabei behandeln wir die verschiedenen Arten von Gerätetreibern und sehen uns an, wie sie geladen und initialisiert werden und wie sie E/A-Anforderungen verarbeiten. Anschließend beschäftigen wir uns mit der Funktionsweise und dem Zweck der PNP- und der Energieverwaltung. IRQ-Ebenen und verzögerte Prozeduraufrufe Für unsere weitere Erörterung müssen wir zunächst zwei wichtige Elemente des Windows-Kernels vorstellen, die eine große Rolle für das E/A-System spielen, nämlich IRQ-Ebenen (Interrupt Request Levels, IRQL) und verzögerte Prozeduraufrufe (Deferred Procedure Calls, DPCs). In Kapitel 8 von Band 2 werden diese Funktionsprinzipien ausführlicher erklärt. Hier wollen wir nur so weit darauf eingehen, wie es für das Verständnis der E/A-Verarbeitung nötig ist. 560 Kapitel 6 Das E/A-System
IRQ-Ebenen Der Begriff »IRQ-Ebene« (IRQL) bedeutet zweierlei, doch in bestimmten Situationen können diese beiden Bedeutungen ein und dasselbe bezeichnen: QQ Eine IRQL ist eine Priorität, die einer Interruptquelle auf einem Hardwaregerät zugewiesen wird Dieser Prioritätswert wird gemeinsam von der HAL und dem Interruptcon­ troller festgelegt, mit dem die Geräte, die eine Interruptverarbeitung benötigen, verbunden sind. QQ Jede CPU verfügt über ihren eigenen IRQL-Wert Dieser Wert kann als ein Register der CPU angesehen werden, auch wenn er in heutigen CPUs nicht auf diese Weise implementiert ist. Die Grundregel lautet, dass Code mit höherer IRQL Code mit niedrigerer IRQL unterbrechen kann, aber nicht umgekehrt. Beispiele dafür folgen in Kürze. In Abb. 6–4 sehen Sie eine Liste von IRQLs für die von Windows unterstützten Architekturen. Beachten Sie, dass IRQLs und Threadprioritäten zwei verschiedene Dinge sind. Threadprioritäten sind nur auf IRQLs kleiner 2 von Bedeutung. Hoch (31) Stromausfall (30) Inter-Processor Interrupt (IPI) (29) Takt (28) Profile (27) Hoch/Profil (15) IPI/Strom (14) Takt (13) Geräte-IRQL (DIRQL) 3 bis 26 Geräte-IRQL (DIRQL) 3 bis 12 Dispatch/DPC (2) Dispatch/DPC (2) APC (1) APC (1) Passiv/niedrig (0) Passiv/niedrig (0) x86 x64 und ARM Thread­ prioritäten Abbildung 6–4 IRQLs Eine IRQL ist nicht dasselbe wie ein IRQ (Interrupt Request). IRQs sind Hardwareleitungen, die Geräte mit einem Interruptcontroller verbinden. Mehr über Interrupts, IRQs und IRQLs erfahren Sie in Kapitel 8 von Band 2. Die IRQL eines Prozessors ist normalerweise 0. Das bedeutet, dass in dieser Beziehung »nichts Besonderes« geschieht und dass der Kernelscheduler, der Threads aufgrund ihrer Prioritäten usw. einplant, wie in Kapitel 4 beschrieben funktioniert. Im Benutzermodus kann die IRQL nur IRQ-Ebenen und verzögerte Prozeduraufrufe Kapitel 6 561
0 sein. Es gibt keine Möglichkeit, die IRQL vom Benutzermodus aus anzuheben. Aus diesem Grund wird die IRQL in der Benutzermodusdokumentation auch nie erwähnt. Kernelmoduscode dagegen kann die IRQL der aktuellen CPU mit den Funktionen KeRaise­ Irql und KeLowerIrql anheben bzw. senken. Die meisten zeitspezifischen Funktionen werden jedoch mit einer IRQL ausgeführt, die auf einen bestimmten Wert angehoben ist, wie Sie bei der Beschreibung der typischen E/A-Verarbeitung durch einen Treiber in Kürze noch sehen werden. Die wichtigsten IRQLs für unsere Erörterung der E/A sind: QQ Passiv (0) Sie wird durch das Makro PASSIVE_LEVEL im WKD-Header wdm.h definiert. Dies ist die Standard-IRQL, bei der der Kernelscheduler auf die übliche, in Kapitel 4 beschriebene Weise arbeitet. QQ Dispatch/DPC (2) (DISPATCH_LEVEL) Dies ist die IRQL des Kernelschedulers selbst. Wenn ein Thread die aktuelle IRQL auf 2 oder höher anhebt, steht ihm im Grunde genommen ein unendliches Quantum zur Verfügung, da er von keinem anderen Thread unterbrochen werden kann. Der Scheduler auf der aktuellen CPU kann erst dann wieder aufwachen, wenn die IRQL unter 2 gefallen ist. Das hat die beiden folgenden Konsequenzen: • Auf IRQL 2 und höher führt jeder Wartevorgang auf Kerneldispatcherobjekte (wie Mutexe, Semaphoren und Ereignisse) zu einem Absturz des Systems, da der Thread dazu in einen Wartezustand eintreten und ein anderer Thread auf derselben CPU eingeplant werden müsste. Das aber kann nicht geschehen, da sich der Scheduler nicht regt, wenn Code auf dieser Ebene ausgeführt wird. Stattdessen reagiert das System mit einem Beendigungsfehler. Die einzige Ausnahme liegt vor, wenn das Wartetimeout auf 0 gesetzt ist, denn dies bedeutet, dass in Wirklichkeit gar keine Wartezeit erforderlich ist, sondern nur der signalisierte Zustand des Objekts abgerufen werden muss. • Es können keine Seitenfehler verarbeitet werden, denn sie würden einen Kontextwechsel zu einem der Schreibthreads für geänderte Seiten erfordern. Da Kontextwechsel aber nicht zulässig sind, stürzt das System ab. Code, der auf IRQL 2 oder höher läuft, kann daher nur auf nicht ausgelagerten Speicher zugreifen, also gewöhnlich auf Speicher aus dem nicht ausgelagerten Pool, der sich definitionsgemäß stets im physischen Speicher befindet. QQ Geräte-IRQL (3 bis 26 auf x86, x bis 12 auf x64 und ARM) (DIRQL) Dies sind die Ebenen für Hardwareinterrupts. Trifft ein solcher Interrupt ein, so ruft der Trapdispatcher des Kernels die entsprechende Interruptdienstroutine (Interrupt Service Routine, ISR) auf und hebt deren IRQL auf den des Interrupts an. Da dieser Wert stets höher als DISPATCH_LEVEL (also 2) ist, gelten für DIRQL auch alle Regeln von IRQL 2. Bei der Ausführung auf einer bestimmten IRQL werden Interrupts derselben oder einer niedrigeren IRQL verdeckt. Wird beispielsweise eine ISR auf IRQL 8 ausgeführt, so kann sie auf der vorliegenden CPU nicht von Code mit IRQL 7 und niedriger unterbrochen werden. Insbesondere kann kein Benutzermoduscode ausgeführt werden, da dessen IRQL stets 0 beträgt. Die Ausführung auf einer hohen IRQL ist daher im Allgemeinen nicht wünschenswert. Es gibt nur einige wenige besondere Fälle (die wir uns in diesem Kapitel noch ansehen werden), in denen sie sinnvoll und für den Normalbetrieb des Systems auch erforderlich ist. 562 Kapitel 6 Das E/A-System
Verzögerte Prozeduraufrufe Ein verzögerter Prozeduraufruf (Deferred Procedure Call, DPC) ist ein Objekt, das den Aufruf einer Funktion auf IRQL 2 (DPC_LEVEL) kapselt. DPCs sind hauptsächlich für die Verarbeitung im Anschluss an einen Interrupt da, denn bei der Ausführung auf ihrer IRQL werden alle Interrupts, die auf Verarbeitung warten, verdeckt und damit verzögert. Eine ISR führt gewöhnlich nur das Minimum an erforderlicher Arbeit aus – was in den meisten Fällen heißt, dass sie den Status des Geräts liest und es anweist, sein Interruptsignal zu beenden – und verschiebt die weitere Verarbeitung dann auf eine niedrigere IRQL (2), indem sie einen DPC anfordert. Verzögert bedeutet, dass der DPC nicht sofort ausgeführt wird; was er auch gar nicht kann, da die IRQL zurzeit immer noch höher als 2 ist. Wenn die ISR die Steuerung zurückgibt und keine weiteren ausstehenden Interrupts auf Verarbeitung warten, fällt die CPU-IRQL auf 2, sodass die bis dahin angesammelten DPCs ausgeführt werden. Abb. 6–5 zeigt einen vereinfachten Überblick über den Ablauf der Ereignisse, wenn ein Interrupt von einem Hardwaregerät auftritt (was jederzeit geschehen kann, da diese Interrupts von Natur aus asynchron sind), während der Code auf der regulären IRQL 0 ausgeführt wird. Benutzer-/Kernel­ moduscode (0) ISR 1 (5) ISR 2 (8) DPC in Warte­ schlange gestellt DPCVerarbei­tungs­ schleife DPC in Warte­ schlange gestellt DPC-Warte­ schlange DPC DPC-Routine 1 DPC DPC-Routine 2 Abbildung 6–5 Interrupt- und DPC-Verarbeitung In Abb. 6–5 geschieht Folgendes: 1. Benutzer- oder Kernelmoduscode wird ausgeführt, während sich die CPU auf IRQL 0 befindet, was meistens der Fall ist. 2. Ein Hardwareinterrupt mit IRQL 5 trifft ein (Geräte-IRQLs sind immer mindestens 3). Da dieser Wert höher ist als der der aktuellen IRQL, wird der CPU-Status gespeichert, die IRQL auf 5 erhöht und die mit dem Interrupt verknüpfte ISR aufgerufen. Es findet jedoch kein Kontextwechsel statt. Der ISR-Code wird von dem bisherigen Thread ausgeführt. Falls sich der Thread im Benutzermodus befand, wird er beim Eintreffen eines Interrupts in den Kernelmodus umgeschaltet. IRQ-Ebenen und verzögerte Prozeduraufrufe Kapitel 6 563
3. ISR 1 beginnt mit der Ausführung, während sich die CPU nach wie vor auf IRQL 5 befindet. Interrupts auf IRQL 5 oder niedriger können jetzt keine Unterbrechung mehr hervorrufen. 4. Ein Interrupt mit IRQL 8 trifft ein. Falls das System entscheidet, dass sich dieselbe CPU darum kümmern soll, wird der Code erneut unterbrochen, da die IRQL 8 höher ist als die aktuelle. Abermals wird der CPU-Status gespeichert, die IRQL wird auf 8 erhöht und die CPU springt zu ISR 2. Die Ausführung erfolgt wiederum im selben Thread. Da der Threadscheduler auf IRQL 2 und höher nicht aufwachen kann, sind keine Kontextwechsel möglich. 5. ISR 2 wird ausgeführt. Zur vollständigen Verarbeitung des Interrupts sind noch einige Arbeiten auf einer niedrigeren IRQL erforderlich, sodass Code mit IRQL kleiner 8 wieder verarbeitet werden kann. 6. Als Letztes ruft ISR 2 die Funktion KeInsertQueueDpc auf, um einen DPC in die Warteschlange zu stellen. Dieser DPC zeigt auf eine Treiberroutine, die die weitere Verarbeitung nach dem Verwerfen des Interrupts ausführt. Welche Aufgaben eine solche weitere Verarbeitung gewöhnlich einschließt, sehen wir uns im nächsten Abschnitt an. Die ISR gibt die Steuerung zurück. Der CPU-Status vor ihrem Aufruf wird wiederhergestellt. 7. Die IRQL fällt wieder auf den vorherigen Wert von 5, sodass die CPU mit der Ausführung der unterbrochenen ISR 1 fortfährt. 8. Kurz bevor ISR 1 zum Ende kommt, stellt sie einen eigenen DPC für die weitere Verarbeitung in die Warteschlange. Anschließend gibt ISR 1 die Steuerung zurück. Der CPU-Status vor ihrem Aufruf wird wiederhergestellt. 9. Die IRQL sollte jetzt auf den alten Wert von 0 fallen, der vor dem Eintreffen der Interrupts vorherrschte. Der Kernel erkennt aber, dass noch ausstehende DPCs vorhanden sind, weshalb er die IRQL auf 2 senkt (DPC_LEVEL) und in eine DPC-Verarbeitungsschleife eintritt, die die gesammelten DPCs durchläuft und nacheinander die einzelnen DPC-Routinen aufruft. Wenn die DPC-Warteschlange komplett abgearbeitet ist, endet die DPC-Verarbeitung. 10. Jetzt kann die IRQL endlich auf 0 zurückfallen. Der ursprüngliche CPU-Zustand wird wiederhergestellt, und die Ausführung des unterbrochenen Benutzer- oder Kernelmoduscodes wird wieder aufgenommen. Die gesamte Verarbeitung lief in demselben Thread ab. Das bedeutet, dass ISRs und DPC-Routinen nicht auf einen bestimmten Thread (und damit auch nicht auf einen bestimmten Prozess) angewiesen sein dürfen. Die Ausführung kann in jedem beliebigen Thread stattfinden. Die Bedeutung dieses Umstands sehen wir uns im nächsten Abschnitt noch an. Gerätetreiber Um mit dem E/A-Manager und anderen Komponenten des E/A-Systems zusammenwirken zu können, muss ein Gerätetreiber den Implementierungsrichtlinien für die Art des Geräts genügen, die er verwaltet. In diesem Abschnitt sehen wir uns die verschiedenen von Windows unterstützten Arten von Gerätetreibern und deren internen Aufbau an. 564 Kapitel 6 Das E/A-System
Die meisten Kernelmodus-Gerätetreiber sind in C geschrieben. Dank der besonderen Unterstützung für C++-Kernelmoduscode in den neuen Compilern können sie beginnend mit dem Windows Driver Kit 8.0 auch gefahrlos in C++ geschrieben werden. Von der Verwendung von Assemblersprache wird jedoch dringend abgeraten, da sie die Treiber komplizierter macht und ihre Portierung zwischen den von Windows unterstützten Hardwarearchitekturen (x86, x64 und ARM) erschwert. Arten von Gerätetreibern Windows unterstützt eine breite Palette von Gerätetreibertypen und Programmierumgebungen. Selbst innerhalb einer bestimmten Art von Gerätetreiber sind je nach Gerät unterschiedliche Programmierumgebungen möglich. Die gröbste Einteilung ist die in Benutzer- und Kernelmodustreiber. Windows unterstützt zwei Arten von Benutzermodustreiber: QQ Druckertreiber des Windows-Teilsystems Sie übersetzen geräteunabhängige Grafikanforderungen in druckerspezifische Befehle. Diese Befehle wiederum werden dann an einen Kernelmodus-Porttreiber wie den für den USB-Druckeranschluss weitergeleitet (­Usbprint.sys). QQ UMDF-Treiber (User-Mode Driver Framework) Hierbei handelt es sich um Hardwaregerätetreiber, die im Benutzermodus ausgeführt werden können und über ALPCS mit der UMDF-Bibliothek im Kernelmodus kommunizieren. Mehr darüber erfahren Sie im Abschnitt »User-Mode Driver Framework« weiter hinten in diesem Kapitel. In diesem Kapitel befassen wir uns jedoch hauptsächlich mit Kernelmodustreibern. Es gibt verschiedene Arten davon, die sich in die folgenden Grundkategorien einteilen lassen: QQ Dateisystemtreiber Diese Treiber nehmen E/A-Anforderungen für Dateien entgegen und geben zu deren Verarbeitung ihre eigenen ausdrücklichen Anforderungen an Massenspeicher- und Netzwerkgerätetreiber aus. QQ Plug-&-Play-Treiber Diese Treiber arbeiten mit der Hardware und dem Energie- und dem PnP-Manager zusammen. Dazu gehören Treiber für Massenspeichergeräte, Video­ adapter, Eingabegeräte und Netzwerkkarten. QQ Nicht-Plug-&-Play-Treiber Dazu gehören Kernelerweiterungen, also Treiber und Module, die den Funktionsumfang des Systems erweitern. Gewöhnlich arbeiten sie nicht mit dem PnP-Manager und der Energieverwaltung zusammen, da sie meistens keine tatsächliche Hardware verwalten. Beispiele für diese Kategorie sind Netzwerk-API- und Protokolltreiber. Das Sysinternals-Tool Process Monitor verfügt über einen Treiber, der ebenfalls in diese Gruppe fällt. Die Kernelmodustreiber können auch auf der Grundlage des verwendeten Treibermodells und ihrer Rolle bei der Verarbeitung ihrer Geräteanforderungen unterteilt werden. Gerätetreiber Kapitel 6 565
WDM-Treiber Bei WDM-Treibern handelt es sich um Gerätetreiber, die dem Windows Driver Model folgen. Dieses Modell unterstützt Energieverwaltung, Plug & Play und WMI, und die meisten Plug-&Play-Treiber folgen ihm. Es gibt drei Arten von WDM-Treibern: QQ Bustreiber Sie verwalten einen logischen oder physischen Bus, beispielsweise PCMCIA, PCI, USB und IEEE 1394. Ein Bustreiber erkennt an seinen Bus angeschlossene Geräte und informiert den PnP-Manager darüber. Außerdem ist er für die Energieeinstellungen des Busses zuständig. Diese Treiber werden gewöhnlich von Microsoft mitgeliefert. QQ Funktionstreiber Sie verwalten jeweils eine bestimmte Art von Gerät. Der Bustreiber zeigt das Gerät über den PnP-Manager dem Funktionstreiber an, und der Funktionstreiber wiederum stellt die Betriebsschnittstelle des Geräts im Betriebssystem bereit. Im Allgemeinen ist der Funktionstreiber derjenige, der die meisten Kenntnisse über das Gerät hat. QQ Filtertreiber Diese Treiber sind logisch über den Funktionstreibern (obere Filter oder Funktionsfilter) oder dem Bustreiber angeordnet (untere Filter oder Busfilter) und erweitern oder ändern das Verhalten eines Geräts oder eines anderen Treibers. Beispielsweise kann ein Hilfsprogramm zur Tastaturerfassung mit einem Tastaturfiltertreiber implementiert werden, der oberhalb des Tastaturfunktionstreibers liegt. Abb. 6–6 zeigt einen Geräteknoten mit einem Bustreiber, der ein physisches Geräteobjekt erstellt (Physical Device Objekt, PDO), einem Funktionstreiber, der ein funktionales Geräteobjekt (FDO) erstellt, sowie mit unteren und oberen Filtern. Erforderlich sind dabei nur das PDO und das FDO. Das Vorhandensein der Filter ist optional. Oberer Filtertreiber 3 Oberer Filtertreiber 2 Oberer Filtertreiber 1 Funktionstreiber Unterer Filtertreiber 2 Unterer Filtertreiber 1 Bustreiber Abbildung 6–6 WDM-Geräteknoten In WDM ist kein Treiber für die Steuerung aller Aspekte eines Geräts alleinverantwortlich. Der Bustreiber kümmert sich um Änderungen der Busmitgliedschaft (Hinzufügen und Entfernen von Geräten), hilft dem PnP-Manager beim Aufzählen der Geräte am Bus, greift auf die busspezifischen Konfigurationsregister zu und verwaltet in einigen Fällen auch den Stromverbrauch 566 Kapitel 6 Das E/A-System
der Geräte am Bus. Der Funktionstreiber ist im Allgemeinen der einzige, der auf die Gerätehardware zugreift. Mehr darüber erfahren Sie im Abschnitt »Der PnP-Manager« weiter hinten in diesem Kapitel. Geschichtete Treiber Die Unterstützung für ein einziges Hardwaregerät ist oft auf mehrere Treiber aufgeteilt, die jeweils einen Teil der erforderlichen Funktionalität bereitstellen. Neben den Bus-, Funktionsund Filtertreibern von WDM kann sich die Hardwareunterstützung auch noch auf folgende Komponenten ausdehnen: QQ Klassentreiber Sie implementieren jeweils die E/A-Verarbeitung für eine ganze Klasse von Geräten, z. B. Festplatten, Tastaturen oder CD-ROM-Laufwerke, bei denen die Hardwareschnittstellen standardisiert sind, sodass ein Treiber Geräte verschiedener Hersteller steuern kann. QQ Miniklassentreiber Sie implementieren die herstellerspezifische E/A-Verarbeitung für jeweils eine bestimmte Geräteklasse. Obwohl es einen standardisierten Batterieklassen-Treiber von Microsoft gibt, haben beispielsweise sowohl unterbrechungsfreie Stromversorgungen (USV) als auch Laptopakkus sehr besondere und stark herstellerabhängige Schnittstellen, weshalb ein Miniklassentreiber vom Hersteller erforderlich ist. Bei diesen Treibern handelt es sich im Grunde genommen um Kernelmodus-DLLs, die keine direkte IRP-Verarbeitung durchführen. Stattdessen werden sie von den Klassentreibern aufgerufen und importieren deren Funktionen. QQ Porttreiber Sie implementieren die Verarbeitung von E/A-Anforderungen für eine bestimmte Art von E/A-Port, z. B. SATA, und sind als Kernelmodus-Funktionsbibliotheken statt als Gerätetreiber im eigentlichen Sinne geschrieben. Porttreiber stammen fast ausschließlich von Microsoft, da die Schnittstellen gewöhnlich so standardisiert sind, dass Geräte verschiedener Hersteller denselben Treiber nutzen können. Im Einzelnen kann es jedoch sein, dass Dritthersteller ihren eigenen Porttreiber für besondere Hardware schreiben müssen. Manchmal schließen die E/A-Ports auch logische Ports ein. Beispielsweise ist der NDIS-Treiber (Network Driver Interface Specification) der »Port«-Treiber für das Netzwerk. QQ Miniporttreiber Diese Treiber ordnen eine allgemeine E/A-Anforderung für eine Art von Port einem anderen Adaptertyp zu, beispielsweise einer bestimmten Netzwerkkarte. Miniporttreiber sind echte Gerätetreiber, die die von einem Porttreiber bereitgestellten Funktionen importieren. Sie werden von Drittherstellern geschrieben und stellen eine Schnittstelle für den Porttreiber bereit. Ebenso wie Miniklassentreiber handelt es sich bei ihnen um Kernelmodus-DLLs, die keine direkte IRP-Verarbeitung durchführen. Abb. 6–7 gibt einen vereinfachten Überblick über die Funktionsweise und Schichtung von Gerätetreibern. Der Dateisystemtreiber nimmt hier die Anforderung entgegen, Daten an eine bestimmte Stelle einer bestimmten Datei zu schreiben, und übersetzt sie in eine Anforderung, eine bestimmte Anzahl von Bytes an eine bestimmte (logische) Stelle auf der Festplatte zu schreiben. Anschließend übergibt er sie über den E/A-Manager an einen einfachen Festplattentreiber, der die Stelle wiederum in einen physischen Speicherort auf der Festplatte übersetzt und mit der Festplatte kommuniziert, um die Daten zu schreiben. Gerätetreiber Kapitel 6 567
Anwendung/Dienst Benutzermodus Kernelmodus Schreibt Daten an einem bestimmten Offset in eine Datei Datei­system­treiber E/A-Manager Übersetzt den Byteoffset in der Datei in einen Byteoffset im Volume und ruft über den E/A-Manager den nächsten Treiber auf Ruft den Treiber auf, um Daten am Byteoffset im Volume zu schreiben Festplattentreiber Übersetzt den Byteoffset im Volume in einen Byteoffset auf der Festplatte und schreibt die Daten Abbildung 6–7 Schichtung eines Dateisystem- und eines Festplattentreibers Diese Abbildung zeigt die Arbeitsteilung zwischen zwei geschichteten Treibern. Der E/A-Manager empfängt eine Schreibanforderung relativ zum Anfang einer bestimmten Datei und übergibt sie an den Dateisystemtreiber. Der wiederum übersetzt die Schreiboperation in eine Datei an einem Anfangsspeicherort (eine Sektorengrenze auf der Festplatte) und die Anzahl der zu schreibenden Bytes und ruft den E/A-Manager auf, um diese Anforderung an den Festplattentreiber zu übergeben. Letzterer wiederum übersetzt die Anforderung in einen Speicherort auf der physischen Festplatte und überträgt die Daten. Da alle Treiber – sowohl Geräte- als auch Dateisystemtreiber – dem Betriebssystem gegenüber dasselbe Framework verwenden, lässt sich in diese Hierarchie problemlos ein weiterer Treiber einfügen, ohne dass die bestehenden Treiber oder das E/A-System geändert werden müssten. Beispielsweise ist es durch einen zusätzlichen Treiber möglich, mehrere Festplatten wie eine einzige große Festplatte erscheinen zu lassen. Ein solcher Treiber zur Verwaltung eines logischen Volumes befindet sich zwischen dem Dateisystemtreiber und den Festplattentreibern, wie Sie in Abb. 6–8 sehen. (Diagramme für den eigentlichen Speichertreiber und den Volumetreiber finden Sie in Kapitel 12 von Band 2.) 568 Kapitel 6 Das E/A-System
Anwendung/Dienst Benutzermodus Kernelmodus Schreibt Daten an einem bestimmten Offset in eine Datei Datei­system­treiber E/A-Manager Übersetzt den Byteoffset in der Datei in einen Byteoffset im Volume und ruft über den E/A-Manager den nächsten Treiber auf Ruft den Treiber auf, um Daten am Byteoffset im Volume zu schreiben Volume­verwaltungs­ treiber Übersetzt den Byteoffset im Volume in eine Laufwerksnummer und einen Offset und ruft über den E/A-Manager den nächsten Treiber auf Ruft den nächsten Treiber auf, um die Daten am Byteoffset auf der Festplatte auf Festplatte 3 zu schreiben Festplattentreiber Übersetzt den Byteoffset auf der Festplatte in einen physischen Speicherort auf Festplatte 3 und schreibt die Daten Abbildung 6–8 Hinzufügen eines weiteren geschichteten Treibers Gerätetreiber Kapitel 6 569
Experiment: Die Liste der geladenen Treiber einsehen Sie können sich eine Liste registrierter Treiber ansehen, indem Sie im Dialogfeld Ausführen im Startmenü das Hilfsprogramm Msinfo32.exe ausführen und dann Systemtreiber unter Softwareumgebung markieren. Die tatsächlich geladenen Treiber sind in der Spalte Gestartet mit Ja gekennzeichnet: Diese Liste stammt aus den Unterschlüsseln von HKLM\System\CurrentControlSet\Services, wo sowohl Treiber als auch Dienste aufgeführt werden. Beide können über den Dienststeuerungsmanager (Service Control Manager, SCM) gestartet werden. Um zu erkennen, ob es sich bei einem Unterschlüssel um einen Treiber oder einen Dienst handelt, schauen Sie sich den Wert von Type an. Ein kleiner Wert (1, 2, 4, 8) weist auf einen Treiber hin, die Werte 16 (0x10) und 32 (0x20) dagegen auf einen Windows-Dienst. Weitere Informationen über den Schlüssel Services erhalten Sie in Kapitel 9 von Band 2. Die Liste der geladenen Kernelmodustreiber können Sie sich auch in Process Explorer ansehen. Wählen Sie dort den Prozess System, öffnen Sie das Menü View und wählen Sie dort Lower Pane View und dann DLLs. → 570 Kapitel 6 Das E/A-System
Process Explorer führt die geladenen Treiber mit ihren Namen, Versionsinformationen (einschließlich Hersteller und Beschreibung) und Ladeadressen auf (sofern Sie Process Explorer zur Anzeige der entsprechenden Spalten eingerichtet haben). Wenn Sie sich im Kerneldebugger ein Absturzabbild oder ein Live-System ansehen, können Sie mit dem Befehl lm kv eine ähnliche Anzeige aufrufen: kd> lm kv start end module name 80626000 80631000 kdcom (deferred) Image path: kdcom.dll Image name: kdcom.dll Browse all global symbols functions data Timestamp: Sat Jul 16 04:27:27 2016 (57898D7F) CheckSum: 0000821A ImageSize: 0000B000 Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4 81009000 81632000 nt (pdb symbols) e:\symbols\ntkrpamp. pdb\A54DF85668E54895982F873F58C984591\ntkrpamp.pdb Loaded symbol image file: ntkrpamp.exe Image path: ntkrpamp.exe Image name: ntkrpamp.exe Browse all global symbols functions data Timestamp: Wed Sep 07 07:35:39 2016 (57CF991B) CheckSum: 005C6B08 ImageSize: 00629000 Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4 81632000 81693000 hal (deferred) Image path: halmacpi.dll Image name: halmacpi.dll Browse all global symbols functions data Timestamp: Sat Jul 16 04:27:33 2016 (57898D85) CheckSum: 00061469 ImageSize: 00061000 Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4 8a800000 8a84b000 FLTMGR (deferred) Image path: \SystemRoot\System32\drivers\FLTMGR.SYS Image name: FLTMGR.SYS Browse all global symbols functions data Timestamp: Sat Jul 16 04:27:37 2016 (57898D89) CheckSum: 00053B90 ImageSize: 0004B000 Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4 ... Gerätetreiber Kapitel 6 571
Aufbau eines Treibers Das E/A-System steuert die Ausführung der Gerätetreiber. Diese Treiber bestehen aus Routinen, die aufgerufen werden, um die verschiedenen Phasen einer E/A-Anforderung zu verarbeiten. Abb. 6–9 zeigt die Haupttreiberroutinen. E/A-System Initialisierungsroutine DPC-Routine Routine zum Hinzu­ fügen von Geräten Dispatchroutinen ISR E/A-Startroutine Abbildung 6–9 Hauptroutinen von Gerätetreibern QQ Initialisierungsroutine Wenn der Treiber in das Betriebssystem geladen wird, legt das WDK GSDriverEntry als Initialisierungsroutine fest, die dann vom E/A-Manager ausgeführt wird. GSDriverEntry initialisiert den Schutz des Compilers gegen Stacküberlauffehler (ein sogenanntes Cookie) und ruft dann die Routine DriverEntry auf, die der Treiberautor implementieren muss. Sie füllt Systemdatenstrukturen aus, um die restlichen Treiberroutinen beim E/A-Manager zu registrieren, und führt alle nötigen globalen Treiberinitialisierungsarbeiten durch. QQ Routine zum Hinzufügen von Geräten Ein Treiber, der Plug & Play unterstützt, implementiert eine Routine zum Hinzufügen von Geräten. Über diese Routine sendet der PnP-Manager eine Benachrichtigung an den Treiber, wenn ein Gerät erkannt wird, für das der Treiber zuständig ist. Der Treiber erstellt in dieser Routine gewöhnlich ein Geräteobjekt (siehe weiter hinten in diesem Kapitel), das für das Gerät steht. QQ Dispatchroutinen Dispatchroutinen sind die Haupteintrittspunkte, die ein Gerätetreiber bereitstellt. Darunter gibt es Routinen zum Öffnen, Schließen, Lesen und Schreiben und für Plug & Play. Wenn der E/A-Manager aufgerufen wird, um eine E/A-Operation durchzuführen, erstellt er ein IRP und ruft einen Treiber durch eine von dessen Dispatchroutinen auf. QQ E/A-Startroutine Mit einer E/A-Startroutine kann ein Treiber die Datenübertragung von oder zu einem Gerät auslösen. Eine solche Routine wird nur in Treibern definiert, die ihre eingehenden E/A-Anforderungen vom E/A-Manager in eine Warteschlange stellen lassen. Der E/A-Manager serialisiert die IRPs für einen Treiber, indem er dafür sorgt, dass der Treiber immer nur ein IRP auf einmal ausführt. Treiber können zwar mehrere IRPs gleichzeitig ausführen, doch bei den meisten Geräten ist eine Serialisierung erforderlich, da sie nicht in der Lage sind, mehrere E/A-Anforderungen nebeneinander zu handhaben. QQ Interruptdienstroutine (ISR) Wenn ein Gerät einen Interrupt auslöst, überträgt der Interruptdispatcher des Kernels die Steuerung an diese Routine. Im E/A-Modell von Windows werden ISRs auf der Geräte-IRQL ausgeführt (DIRQL). Um Interrupts mit niedrigerer IRQL 572 Kapitel 6 Das E/A-System
nicht zu blockieren, führen sie so wenig Arbeit aus wie möglich (siehe den vorhergehenden Abschnitt). Eine ISR stellt gewöhnlich einen DPC mit niedrigerer IRQL (DPC/Dispatch-Ebene) in die Warteschlange, um die restlichen Aufgaben zur Verarbeitung des Interrupts zu erledigen. Nur Treiber für interruptgesteuerte Geräte verfügen über ISRs. Ein Dateisystemtreiber ist ein Beispiel für einen Treiber ohne ISRs. QQ DPC-Routine zur Interruptverarbeitung Nach der Ausführung der ISR wird die meiste Arbeit zur Verarbeitung eines Geräteinterrupts von einer DPC-Routine erledigt. Sie wird auf IRQL 2 ausgeführt, was einen Kompromiss zwischen der höheren DIRQL und der passiven Ebene 0 darstellt. Eine typische DPC-Routine löst die E/A-Vervollständigung aus und beginnt mit der nächsten E/A-Operation für das Gerät in der Warteschlange. Auch wenn die folgenden Routinen in Abb. 6–9 nicht gezeigt wurden, sind sie in vielen Arten von Gerätetreibern vorhanden: QQ E/A-Vervollständigungsroutinen Ein geschichteter Treiber kann über E/A-Vervollständigungsroutinen verfügen, die ihn informieren, wenn ein Treiber niedrigerer Ebene die Verarbeitung eines IRPs abgeschlossen hat. Beispielsweise ruft der E/A-Manager die E/A-Vervollständigungsroutine eines Dateisystemtreibers auf, nachdem ein Gerätetreiber die Datenübertragung zu oder von einer Datei abgeschlossen hat. Die Vervollständigungsroutine informiert den Dateisystemtreiber, ob die Operation erfolgreich war, fehlgeschlagen ist oder abgebrochen wurde, und ermöglicht ihm, Bereinigungsoperationen auszuführen. QQ E/A-Abbruchroutine Ein Treiber kann auch E/A-Abbruchroutinen definieren. Wenn er ein IRP für eine E/A-Operation empfängt, die abgebrochen werden kann, so weist er diesem IRP eine Abbruchroutine zu. Während das IRP die einzelnen Phasen der Verarbeitung durchläuft, kann sich die Routine ändern oder auch komplett verschwinden, wenn die laufende Operation nicht abgebrochen werden kann. Wird ein Thread, der eine E/A-Anforderung ausgegeben hat, vor Abschluss dieser Anforderung beendet oder wird die Operation abgebrochen (z. B. durch die Windows-Funktionen CancelIo oder CancelIOEx), führt der E/A-Manager ggf. die dem IRP zugewiesene Abbruchroutine aus. Diese Routine erledigt alle erforderlichen Aufgaben, um die bei der bisherigen Verarbeitung des IRPs erworbenen Ressourcen freizugeben und um das IRP mit dem Status »abgebrochen« abzuschließen. QQ Schnelldispatchroutinen Treiber, die den Cache-Manager nutzen – z. B. Dateisystemtreiber – stellen gewöhnlich Routinen dieser Art bereit, damit der Kernel beim Zugriff auf den Treiber die übliche E/A-Verarbeitung umgehen kann. (Mehr über den Cache-Manager erfahren Sie in Kapitel 14 von Band 2.) Beispielsweise können Lese- und Schreiboperationen durch den direkten Zugriff auf die Daten im Cache schneller erledigt werden als mit der üblichen Vorgehensweise des E/A-Managers, bei der diskrete E/A-Operationen erfolgen. Die Schnelldispatchroutinen dienen auch als Callbacks vom Speicher- und Cache-­ Manager zu den Dateisystemtreibern. Wenn der Speicher-Manager beispielsweise einen Abschnitt erstellt, führt er einen Callback zum Dateisystemtreiber durch, um die Datei exklusiv zu erwerben. QQ Entladeroutine Eine Entladeroutine gibt jegliche Systemressourcen frei, die ein Treiber verwendet, sodass der E/A-Manager den Treiber aus dem Arbeitsspeicher entfernen kann. Gerätetreiber Kapitel 6 573
In der Initialisierungsroutine (DriverEntry) erworbene Ressourcen werden gewöhnlich in der Entladeroutine freigegeben. Ein Treiber kann bei laufendem System geladen und entladen werden, sofern er diese Möglichkeit unterstützt, doch die Entladeroutine wird nur dann aufgerufen, wenn alle Dateihandles für das Gerät geschlossen sind. QQ Routine für Benachrichtigungen über das Herunterfahren des Systems Diese Routine ermöglicht eine Treiberbereinigung beim Herunterfahren des Systems. QQ Fehlerprotokollierungsroutinen Wenn unerwartete Fehler auftreten (z. B. wenn ein Festplattenblock beschädigt ist), bemerken die Fehlerprotokollierungsroutinen des Treibers dies und informieren den E/A-Manager, der die entsprechenden Informationen in eine Fehlerprotokolldatei schreibt. Treiber- und Geräteobjekte Wenn ein Thread ein Handle für ein Dateiobjekt öffnet (siehe den Abschnitt »E/A-Verarbeitung« weiter hinten in diesem Kapitel), muss der E/A-Manager anhand des Dateiobjektnamens bestimmen, welchen Treiber er zur Verarbeitung der Anforderung aufrufen soll. Außerdem muss er diese Information wiederfinden, wenn später ein Thread das gleiche Filehandle verwendet. Dazu sind folgende Objekte da: QQ Treiberobjekt Dieses Objekt (DRIVER_OBJECT-Struktur) steht für einen einzelnen Treiber im System. Von diesem Objekt ruft der E/A-Manager die Adressen der einzelnen Dispatchroutinen des Treibers ab (Eintrittspunkte). QQ Geräteobjekt Dieses Objekt (DEVICE_OJBECT-Struktur) steht für ein physisches oder logisches Gerät im System und beschreibt dessen Merkmale, z. B. die erforderliche Ausrichtung für Puffer und den Speicherort der Gerätewarteschlange für eingehende IRPs. Es ist das Ziel für alle E/A-Operationen, da es dieses Objekt ist, mit dem das Handle kommuniziert. Wenn ein Treiber in das System geladen wird, erstellt der E/A-Manager ein Treiberobjekt dafür. Anschließend ruft er die Initialisierungsroutine dieses Treibers auf (DriverEntry), die die Objektattribute mit den Eintrittspunkten des Treibers füllt. Nach dem Laden kann der Treiber jederzeit Geräteobjekte für logische oder physische Geräte erstellen – und sogar für logische Schnittstellen und Endpunkte des Treibers –, indem er IoCreateDevice oder IoCreateDeviceSecure aufruft. Die meisten Plug-&-Play-Treiber erstellen Geräteobjekte jedoch in ihrer Routine zum Hinzufügen von Geräten, wenn der PnP-Manager sie über das Vorhandensein eines Geräts informiert, für das sie zuständig sind. Nicht-Plug&-Play-Treiber dagegen legen die Geräteobjekte an, wenn der E/A-Manager ihre Initialisierungsroutine aufruft. Der E/A-Manager entlädt einen Treiber, wenn dessen letztes Geräteobjekt gelöscht wurde und keine Verweise auf den Treiber mehr übrig sind. Die Beziehung zwischen einem Treiberobjekt und seinen Geräteobjekten ist in Abb. 6–10 dargestellt. 574 Kapitel 6 Das E/A-System
Treiberobjekt Geräteobjekt Geräteobjekt Einzelheiten der Geräteerweiterung Abbildung 6–10 Ein Treiberobjekt und seine Geräteobjekte Das Element DeviceObject eines Treiberobjekts enthält einen Zeiger auf dessen erstes Geräteobjekt. In dieser DEVICE_OBJECT-Struktur befindet sich das Element NextDevice mit einem Zeiger auf das zweite Geräteobjekt usw., bis das Element NextDevice des letzten Geräteobjekts auf NULL zeigt. Im Element DriverObject zeigt jedes Geräteobjekt zurück auf sein Zeigerobjekt. Alle Beziehungen, die in Abb. 6–10 durch Pfeile dargestellt sind, werden von den Funktionen zur Geräteerstellung aufgebaut (IoCreateDevice und IoCreateDeviceSecure). Der Zeiger DeviceExtension bietet einem Treiber die Möglichkeit, zusätzlichen Speicher zuzuweisen, der mit den einzelnen von ihm verwalteten Geräteobjekten verknüpft ist. Die Unterscheidung zwischen Treiber- und Geräteobjekten ist sehr wichtig. Ein Treiberobjekt steht für das Verhalten des Treibers, wohingegen die einzelnen Geräteobjekte die Kommunikationsendpunkte darstellen. Beispielsweise gibt es auf einem System mit vier seriellen Endpunkten ein Treiberobjekt (und eine Binärdatei für den Treiber), aber vier Geräteobjekte, die für die einzelnen seriellen Ports stehen und die einzeln geöffnet werden können, ohne dass sich das auf die anderen seriellen Ports auswirken würde. Auch bei Hardwaregeräten steht jedes Geräteobjekt für einen eigenen Satz von Hardwareressourcen wie E/A-Ports, im Speicher zugeordnete E/A und die Interruptleitung. Windows ist eher geräte- denn treiberorientiert. Wenn ein Treiber ein Geräteobjekt erstellt, kann er es optional benennen. Mit einem solchen Namen gelangt das Geräteobjekt in den Namensraum des Objekt-Managers. Der Treiber kann den Namen entweder ausdrücklich festlegen oder ihn automatisch vom E/A-Manager erstellen lassen. Vereinbarungsgemäß werden Geräteobjekte im Verzeichnis \Device des Namensraums untergebracht, das für Anwendungen über die Windows-API nicht zugänglich ist. Gerätetreiber Kapitel 6 575
Manche Treiber stellen Geräteobjekte auch in andere Verzeichnisse als \Device. Der IDE-Treiber legt z. B. Objekte für IDE-Ports und -Kanäle in \Device\Ide an. Eine Beschreibung der Speicherarchitektur und der Speicherung von Geräteobjekten durch Treiber erhalten Sie in Kapitel 12 von Band 2. Muss ein Treiber Anwendungen das Öffnen eines Geräteobjekts ermöglichen, so muss er mit der Funktion IoCreateSymbolicLink im Verzeichnis \GLOBAL?? eine symbolische Verknüpfung zum Namen des Geräteobjekts in \Device erstellen. Nicht-Plug-&-Play-Treiber und Dateisystemtreiber legen gewöhnlich eine symbolische Verknüpfung mit einem Standardnamen an, z. B. \Device\HarddiskVolume2. Da Standardnamen in Umgebungen, in denen Hardware dynamisch hinzukommt und wieder entfernt wird, nicht funktionieren, stellen PnP-Treiber mithilfe von IoRegisterDeviceInterface Schnittstellen bereit, wobei sie einen global eindeutigen Bezeichner (GUID) für die betreffende Funktionalität angeben. Bei diesen GUIDs handelt es sich um 128-Bit-Werte, die mit den im WDK und im Windows SDK enthaltenen Tools wie uuidgen und guidgen erzeugt werden können. Angesichts des Wertebereichs, der eine Kombination von 128 Bits hat, und der Formel zur Generierung dieser GUIDs ist es statistisch praktisch sicher, dass jeder GUID stets und überall eindeutig ist. IoRegisterDeviceInterface generiert die symbolische Verknüpfung für die Geräteinstanz. Ein Treiber muss jedoch IoSetDeviceInterfaceState aufrufen, um die Schnittstelle zu dem Gerät zu aktivieren, bevor der E/A-Manager die Verknüpfung tatsächlich erstellen kann. Das erledigt der Treiber gewöhnlich, wenn der PnP-Manager das Gerät startet, indem er dem Treiber ein Gerätestart-IRP sendet, in diesem Fall IRP_MJ_PNP (Hauptfunktionscode) mit IRP_MIN_START_­ DEVICE (Nebenfunktionscode). IRPs werden im Abschnitt »E/A-Anforderungspakete« weiter hinten in diesem Kapitel genauer vorgestellt. Möchte eine Anwendung ein Geräteobjekt öffnen, dessen Schnittstellen jeweils einen GUID aufweisen, kann sie Plug-&-Play-Einrichtungsfunktionen wie SetupDiEnumDeviceInterfaces im Benutzerraum aufrufen, um die für einen bestimmten GUID vorhandenen Schnittstellen aufzulisten und die Namen der symbolischen Links abzurufen, die sie zum Öffnen der Geräteobjekte benötigt. Für jedes von SetupDiEnumDeviceInterfaces gemeldete Gerät führt die Anwendung SetupDiGetDeviceInterfaceDetail aus, um zusätzliche Informationen über das Gerät abzurufen, z. B. ob es sich um einen automatisch generierten Namen handelt. Nachdem die Anwendung den Gerätenamen von SetupDiGetDeviceInterfaceDetail erhalten hat, kann sie die Windows-Funktion CreateFile oder CreateFile2 ausführen, um das Gerät zu öffnen und ein Handle zu beziehen. 576 Kapitel 6 Das E/A-System
Experiment: Geräteobjekte beobachten Mit dem Sysinternals-Tool WinObj und mit dem Kerneldebuggerbefehl !object können Sie sich die Gerätenamen im Verzeichnis \Device des Objekt-Manager-Namensraums ansehen. Der folgende Screenshot zeigt eine vom E/A-Manager zugewiesene symbolische Verknüpfung, die auf ein Geräteobjekt mit einem automatisch generierten Namen in \Device zeigt: Wenn Sie den Kerneldebuggerbefehl !object ausführen und dabei das Verzeichnis \Device angeben, erhalten Sie folgende Ausgabe: 1: kd> !object \device Object: 8200c530 Type: (8542b188) Directory ObjectHeader: 8200c518 (new version) HandleCount: 0 PointerCount: 231 Directory Object: 82007d20 Name: Device Hash Address Type Name ---- ------- ---- ---- 00 d024a448 Device NisDrv 959afc08 Device SrvNet 958beef0 Device WUDFLpcDevice 854c69b8 Device FakeVid1 8befec98 Device RdpBus 88f7c338 Device Beep 89d64500 Device Ndis 8a24e250 SymbolicLink ScsiPort2 89d6c580 Device KsecDD 89c15810 Device 00000025 89c17408 Device 00000019 → Gerätetreiber Kapitel 6 577
01 02 03 04 854c6898 Device FakeVid2 88f98a70 Device Netbios 8a48c6a8 Device NameResTrk 89c2fe88 Device 00000026 854c6778 Device FakeVid3 8548fee0 Device 00000034 8a214b78 SymbolicLink Ip 89c31038 Device 00000027 9c205c40 Device 00000041 854c6658 Device FakeVid4 854dd9d8 Device 00000035 8d143488 Device Video0 8a541030 Device KeyboardClass0 89c323c8 Device 00000028 8554fb50 Device KMDF0 958bb040 Device ProcessManagement 97ad9fe0 SymbolicLink MailslotRedirector 854f0090 Device 00000036 854c6538 Device FakeVid5 8bf14e98 Device Video1 8bf2fe20 Device KeyboardClass1 89c332a0 Device 00000029 89c05030 Device VolMgrControl 89c3a1a8 Device VMBus ... Wenn Sie bei dem Befehl !object ein Objekt-Manager-Verzeichnis angeben, gibt der Kerneldebugger den Inhalt dieses Verzeichnisses in der Weise aus, wie der Objekt-Manager ihn intern gliedert. Um schnelles Nachschlagen zu erlauben, sind die Objekte in einem Verzeichnis in einer Hashtabelle nach dem Hash des Objektnamens abgelegt. Daher werden die Objekte in der Ausgabe jeweils in den einzelnen Buckets dieser Hashtabelle gezeigt. Wie Sie in Abb. 6–10 sehen, zeigt ein Geräteobjekt zurück auf sein Treiberobjekt. Dadurch kann der E/A-Manager erkennen, welche Treiberroutine er beim Empfang einer E/A-Anforderung aufrufen muss. Über das Geräteobjekt findet er das Treiberobjekt für den Treiber, der für das Gerät zuständig ist. Anschließend sucht er den richtigen Index im Treiberobjekt, indem er den in der ursprünglichen Anforderung angegebenen Funktionscode verwendet. Jeder Funktionscode entspricht einem Treibereintrittspunkt (also einer Dispatchroutine). Mit einem Treiberobjekt sind oft mehrere Geräteobjekte verknüpft. Wird ein Treiber aus dem System entladen, bestimmt der E/A-Manager anhand der Warteschlange der Geräteobjekte, welche Geräte vom Entfernen des Treibers betroffen sind. 578 Kapitel 6 Das E/A-System
Experiment: Treiber- und Geräteobjekte anzeigen Mit den Kerneldebuggerbefehlen !drvobj und !devobj können Sie sich Treiber- bzw. Geräteobjekte anzeigen lassen. Im folgenden Beispiel untersuchen wir das Treiberobjekt für den Treiber der Tastaturklasse und zeigen eines seiner Geräteobjekte an: 1: kd> !drvobj kbdclass Driver object (8a557520) is for: \Driver\kbdclass Driver Extension List: (id , addr) Device Object list: 9f509648 8bf2fe20 8a541030 1: kd> !devobj 9f509648 Device object (9f509648) is for: KeyboardClass2 \Driver\kbdclass DriverObject 8a557520 Current Irp 00000000 RefCount 0 Type 0000000b Flags 00002044 Dacl 82090960 DevExt 9f509700 DevObjExt 9f5097f0 ExtensionFlags (0x00000c00) DOE_SESSION_DEVICE, DOE_DEFAULT_SD_PRESENT Characteristics (0x00000100) FILE_DEVICE_SECURE_OPEN AttachedTo (Lower) 9f509848 \Driver\terminpt Device queue is not busy. Der Befehl !devobj zeigt auch die Adressen und Namen jeglicher Geräteobjekte an, über denen sich das betrachtete Objekt befindet (siehe die Zeile AttachedTo). Befinden sich Objekte oberhalb des betrachteten Objekts, werden sie in der Zeile AttachedDevice angegeben, was hier aber nicht der Fall ist. Um sich noch mehr Informationen anzeigen zu lassen, können Sie den Befehl !devobj auch mit einem optionalen Argument aufrufen. Im folgenden Beispiel wird damit das Maximum an verfügbaren Informationen abgerufen: 1: kd> !drvobj kbdclass 7 Driver object (8a557520) is for: \Driver\kbdclass Driver Extension List: (id , addr) Device Object list: 9f509648 8bf2fe20 8a541030 DriverEntry: 8c30a010 kbdclass!GsDriverEntry DriverStartIo: 00000000 DriverUnload: 00000000 AddDevice: 8c307250 kbdclass!KeyboardAddDevice → Gerätetreiber Kapitel 6 579
Dispatch routines: [00] IRP_MJ_CREATE 8c301d80 kbdclass!KeyboardClassCreate [01] IRP_MJ_CREATE_NAMED_PIPE 81142342 nt!IopInvalidDeviceRequest [02] IRP_MJ_CLOSE 8c301c90 kbdclass!KeyboardClassClose [03] IRP_MJ_READ 8c302150 kbdclass!KeyboardClassRead [04] IRP_MJ_WRITE 81142342 nt!IopInvalidDeviceRequest [05] IRP_MJ_QUERY_INFORMATION 81142342 nt!IopInvalidDeviceRequest [06] IRP_MJ_SET_INFORMATION 81142342 nt!IopInvalidDeviceRequest [07] IRP_MJ_QUERY_EA 81142342 nt!IopInvalidDeviceRequest [08] IRP_MJ_SET_EA 81142342 nt!IopInvalidDeviceRequest [09] IRP_MJ_FLUSH_BUFFERS 8c303678 kbdclass!KeyboardClassFlush [0a] IRP_MJ_QUERY_VOLUME_INFORMATION 81142342 nt!IopInvalidDeviceRequest [0b] IRP_MJ_SET_VOLUME_INFORMATION 81142342 nt!IopInvalidDeviceRequest [0c] IRP_MJ_DIRECTORY_CONTROL 81142342 nt!IopInvalidDeviceRequest [0d] IRP_MJ_FILE_SYSTEM_CONTROL 81142342 nt!IopInvalidDeviceRequest [0e] IRP_MJ_DEVICE_CONTROL 8c3076d0 kbdclass!KeyboardClassDevice 8c307ff0 kbdclass!KeyboardClassPass [10] IRP_MJ_SHUTDOWN 81142342 nt!IopInvalidDeviceRequest [11] IRP_MJ_LOCK_CONTROL 81142342 nt!IopInvalidDeviceRequest [12] IRP_MJ_CLEANUP 8c302260 kbdclass!KeyboardClassCleanup [13] IRP_MJ_CREATE_MAILSLOT 81142342 nt!IopInvalidDeviceRequest [14] IRP_MJ_QUERY_SECURITY 81142342 nt!IopInvalidDeviceRequest [15] IRP_MJ_SET_SECURITY 81142342 nt!IopInvalidDeviceRequest [16] IRP_MJ_POWER 8c301440 kbdclass!KeyboardClassPower [17] IRP_MJ_SYSTEM_CONTROL 8c307f40 kbdclass!KeyboardClassSystem [18] IRP_MJ_DEVICE_CHANGE 81142342 nt!IopInvalidDeviceRequest [19] IRP_MJ_QUERY_QUOTA 81142342 nt!IopInvalidDeviceRequest [1a] IRP_MJ_SET_QUOTA 81142342 nt!IopInvalidDeviceRequest [1b] IRP_MJ_PNP 8c301870 kbdclass!KeyboardPnP Control [0f] IRP_MJ_INTERNAL_DEVICE_CONTROL Through Control Das Array der Dispatchroutinen ist deutlich zu sehen; wir werden es im nächsten Abschnitt genauer erläutern. Operationen, die der Treiber nicht unterstützt, zeigen auf die Routine IopInvalidDeviceRequest des E/A-Managers. Bei !drvobj geben Sie die Adresse eines DRIVER_OBJECT an und bei !devobj die einer DEVICE_OBJECT-Struktur. Im Debugger können Sie diese Strukturen auch direkt einsehen: 1: kd> dt nt!_driver_object 8a557520 +0x000 Type : 0n4 +0x002 Size : 0n168 +0x004 DeviceObject : 0x9f509648 _DEVICE_OBJECT → 580 Kapitel 6 Das E/A-System
+0x008 Flags : 0x412 +0x00c DriverStart : 0x8c300000 Void +0x010 DriverSize : 0xe000 +0x014 DriverSection : 0x8a556ba8 Void +0x018 DriverExtension : 0x8a5575c8 _DRIVER_EXTENSION +0x01c DriverName : _UNICODE_STRING "\Driver\kbdclass" +0x024 HardwareDatabase : 0x815c2c28 _UNICODE_STRING "\REGISTRY\MACHINE\ HARDWARE\DESCRIPTION\SYSTEM" +0x028 FastIoDispatch : (null) +0x02c DriverInit : 0x8c30a010 long +ffffffff8c30a010 +0x030 DriverStartIo : (null) +0x034 DriverUnload : (null) +0x038 MajorFunction : [28] 0x8c301d80 long +ffffffff8c301d80 1: kd> dt nt!_device_object 9f509648 +0x000 Type : 0n3 +0x002 Size : 0x1a8 +0x004 ReferenceCount : 0n0 +0x008 DriverObject : 0x8a557520 _DRIVER_OBJECT +0x00c NextDevice : 0x8bf2fe20 _DEVICE_OBJECT +0x010 AttachedDevice : (null) +0x014 CurrentIrp : (null) +0x018 Timer : (null) +0x01c Flags : 0x2044 +0x020 Characteristics : 0x100 +0x024 Vpb : (null) +0x028 DeviceExtension : 0x9f509700 Void +0x02c DeviceType : 0xb +0x030 StackSize : 7 '' +0x034 Queue : <unnamed-tag> +0x05c AlignmentRequirement : 0 +0x060 DeviceQueue : _KDEVICE_QUEUE +0x074 Dpc : _KDPC +0x094 ActiveThreadCount : 0 +0x098 SecurityDescriptor : 0x82090930 Void ... In dieser Struktur gibt es einige interessante Felder, die wir uns im nächsten Abschnitt genauer ansehen. Da Informationen über Treiber in Objekten aufgezeichnet werden, muss der E/A-Manager die Einzelheiten der einzelnen Treiber nicht kennen, sondern lediglich einem Zeiger folgen, um den Treiber zu finden. Das sorgt für Portierbarkeit und ermöglicht es, neue Treiber auf einfache Weise zu laden. Gerätetreiber Kapitel 6 581
Geräte öffnen Ein Dateiobjekt ist eine Kernelmodus-Datenstruktur, die für das Handle zu einem Gerät steht. Dateiobjekte erfüllen eindeutig die Kriterien für Objekte in Windows: Es handelt sich um Systemressourcen, die von mehreren Benutzermodusprozessen gemeinsam genutzt werden können; sie können benannt sein; sie sind durch objektgestützte Sicherheitseinrichtungen geschützt; und sie erlauben eine Synchronisierung. Gemeinsame Ressourcen im E/A-System – wie die anderen Komponenten der Windows-Exekutive – werden als Objekte bearbeitet. (Mehr über Objektverwaltung erfahren Sie in Kapitel 8 von Band 2.) Dateiobjekte bieten eine speichergestützte Darstellung von Ressourcen, die über eine E/A-orientierte Schnittstelle gelesen und geschrieben werden können. Tabelle 6–1 führt einige der Attribute von Dateiobjekten auf. Die Deklarationen und Größen der einzelnen Felder schlagen Sie in der Strukturdefinition für FILE_OBJECT in wdm.h nach. Attribut Zweck Dateiname Gibt die virtuelle Datei an, auf die das Dateiobjekt verweist und die an CreateFile oder CreateFile2 übergeben wurde. Aktueller Byteoffset Gibt die aktuelle Stelle in der Datei an (nur gültig für synchrone E/A). Modi für gemeinsame Nutzung Gibt an, ob andere Aufrufer die Datei zum Lesen, Schreiben oder Löschen öffnen können, während der aktuelle Aufrufer sie verwendet. Flags für offenen Modus Gibt an, ob die E/A synchron oder asynchron, mit oder ohne Zwischenspeicherung, sequenziell oder wahlfrei erfolgt usw. Zeiger auf das Geräteobjekt Gibt die Art des Geräts an, auf dem sich die Datei befindet. Zeiger auf den Volumeparameterblock (VPB) Gibt das Volume oder die Partition an, auf dem oder der sich die Datei befindet (bei Dateisystemdateien). Zeiger auf Abschnittsobjektzeiger Gibt die Stammstruktur an, die eine zugeordnete/zwischengespeicherte Datei beschreibt. Diese Struktur enthält auch die gemeinsam genutzte Cachezuordnung, die wiederum angibt, welche Teile der Datei vom Cache-Manager zwischengespeichert (bzw. zugeordnet) sind und wo sie sich im Cache befinden. Zeiger auf private Cachezuordnung Dient zur Speicherung der Cachinginformationen eines Handles, z. B. seine Lesemuster oder die Seitenpriorität für den Prozess. Mehr Informationen über Seitenprioritäten erhalten Sie in Kapitel 5. Liste der E/A-Anforderungspakete (IRPs) Ist das Dateiobjekt bei threadagnostischer E/A mit einem Vervollständigungsport verknüpft, so ist dies eine Liste aller E/A-Operatio­ nen, die mit dem Dateiobjekt verknüpft sind (siehe die Abschnitte »Threadagnostische E/A« und »E/A-Vervollständigungsports« weiter hinten in diesem Kapitel). E/A-Vervollständigungskontext Dies sind die Kontextinformationen für den aktuellen E/A-Vervollständigungsport (falls ein solcher Port aktiv ist). Dateiobjekterweiterung Dient zur Speicherung der E/A-Priorität für die Datei (siehe weiter hinten in diesem Kapitel) sowie der Angabe, ob Prüfungen für den gemeinsamen Zugriff für das Dateiobjekt durchgeführt werden sollen. Dieses Attribut enthält optionale Dateiobjekterweiterungen, in denen kontextspezifische Informationen gespeichert werden. Tabelle 6–1 Attribute von Dateiobjekten 582 Kapitel 6 Das E/A-System
Um gegenüber dem Treibercode, der das Dateiobjekt verwendet, eine gewisse Opazität zu wahren, und um die Funktionalität des Dateiobjekts zu erweitern, ohne die Struktur vergrößern zu müssen, enthält das Dateiobjekt auch ein Feld für Erweiterungen, das bis zu sechs verschiedene weitere Arten von Attributen aufnehmen kann (siehe Tabelle 6–2). Erweiterung Zweck Transaktionsparameter Enthält den Transaktionsparameterblock, der wiederum Informationen über eine als Transaktion ausgeführte Dateioperation enthält. Wird von IoGetTransactionParameterBlock zurückgegeben. Geräteobjekthinweis Bezeichnet das Geräteobjekt des Filtertreibers, mit dem die Datei verknüpft werden soll. Wird mit IoCreateFileEx oder IoCreateFileSpecifyDeviceObjectHint festgelegt. E/A-Statusblockbereich Ermöglicht Anwendungen, einen Benutzermoduspuffer im Kernelspeicher zu verriegeln, um asynchrone E/A zu synchronisieren. Wird mit ­SetFileIoOverlappedRange festgelegt. Allgemein Enthält filtertreiberspezifische Informationen und erweiterte Erstellungsparameter (Extended Create Parameters, ECPs), die vom Aufrufer hinzugefügt wurden. Wird mit IoCreateFileEx festgelegt. Geplante Datei-E/A Speichert Angaben zur Bandbreitenreservierung einer Datei, die vom Speichersystem verwendet werden, um den Durchsatz für Multimediaanwendungen zu optimieren und zu garantieren (siehe den Abschnitt »Bandbreitenreservierung (geplante Datei-E/A)« weiter hinten in diesem Kapitel). Wird mit SetFileBandwidthReservation festgelegt. Symbolische Verknüpfung Wird beim Erstellen des Dateiobjekts hinzugefügt, wenn ein Bereitstellungszeiger oder eine Verzeichnisverzweigung durchlaufen wird (oder ein Filter den Pfad ausdrücklich neu analysiert). In dieser Erweiterung wird der vom Aufrufer angegebene Pfad einschließlich dazwischen liegender Verzweigungen gespeichert, sodass eine relative symbolische Verknüpfung über die Verzweigungen zurückverfolgt werden kann. Mehr Informationen über symbolische NTFS-Verknüpfungen, Bereitstellungspunkte und Verzeichnisverzweigungen erhalten Sie in Kapitel 13 von Band 2. Tabelle 6–2 Erweiterungen für Dateiobjekte Wenn ein Aufrufer eine Datei oder ein einfaches Gerät öffnet, gibt der E/A-Manager ein Handle für das Dateiobjekt zurück. Zuvor wird jedoch der für das betreffende Gerät zuständige Treiber über seine Create-Dispatchroutine (IRP_MJ_CREATE) gefragt, ob es in Ordnung ist, das Gerät zu öffnen. Außerdem wird ihm die Gelegenheit eingeräumt, jegliche Initialisierungen durchzuführen, die für ein erfolgreiches Öffnen erforderlich sind. Dateiobjekte stehen für offene Instanzen von Dateien, nicht für die Dateien selbst. Im Gegensatz zu UNIX-Systemen, die Vnodes verwenden, definiert Windows keine Darstellung einer Datei. Das überlässt Windows den Dateisystemtreibern. Ähnliche wie Exekutivobjekte sind auch Dateien durch einen Sicherheitsdeskriptor mit einer Zugriffssteuerungsliste geschützt. Der E/A-Manager wendet sich an das Sicherheitsteilsystem, um zu bestimmen, ob die Zugriffssteuerungsliste einer Datei den Zugriff auf die Datei in der von dem Thread angeforderten Weise gestattet. Wenn ja, erlaubt der Objekt-Manager den Zugriff und verknüpft die gewährten Zugriffsrechte mit dem Dateihandle, das er zurückgibt. Muss Gerätetreiber Kapitel 6 583
der Thread oder ein anderer Thread in dem Prozess weitere Operationen durchführen, die bei der ursprünglichen Anforderung nicht angegeben waren, so muss er dieselbe Datei mit einer weiteren Anforderung erneut öffnen (oder das Handle mit dem angeforderten Zugriff duplizieren), um ein weiteres Handle zu erhalten, wodurch eine zweite Sicherheitsprüfung ausgelöst wird. Mehr über den Objektschutz erfahren Sie in Kapitel 7. Experiment: Gerätehandles einsehen Jeder Prozess, der ein offenes Handle zu einem Gerät hat, verfügt in seiner Handletabelle über ein Dateiobjekt für die offene Instanz. Diese Handles können Sie in Process Explorer einsehen, indem Sie einen Prozess markieren und dann Handles im Teilmenü Lower Pane View des Menüs View aktivieren. Sortieren Sie anschließend nach der Spalte Type und scrollen Sie zu der Stelle, an der die mit File beschrifteten Handles für die Dateiobjekte angezeigt werden. In diesem Beispiel hat der Prozess Desktop Windows Manager (Dwm.exe) ein offenes Handle zu einem Gerät, das der Kernelsicherheitsgerätetreiber (Ksecdd.sys) erstellt hat. Das entsprechende Dateiobjekt können Sie sich auch im Kerneldebugger ansehen, wozu Sie zunächst seine Adresse ermitteln müssen. Der folgende Befehl gibt Informationen über das im vorherigen Screenshot hervorgehobene Handle aus (Handlewert 0xD4). Dabei handelt es sich um den Prozess Dwm.exe mit einer Prozess-ID von 452 (dezimal): lkd> !handle 348 f 0n452 PROCESS ffffc404b62fb780 SessionId: 1 Cid: 01c4 Peb: b4c3db0000 ParentCid: 0364 DirBase: 7e607000 ObjectTable: ffffe688fd1c38c0 HandleCount: <Data Not Accessible> Image: dwm.exe → 584 Kapitel 6 Das E/A-System
Handle Error reading handle count. 0348: Object: ffffc404b6406ef0 GrantedAccess: 00100003 (Audit) Entry: ffffe688fd396d20 Object: ffffc404b6406ef0 Type: (ffffc404b189bf20) File ObjectHeader: ffffc404b6406ec0 (new version) HandleCount: 1 PointerCount: 32767 Da das Objekt ein Dateiobjekt ist, können Sie Informationen darüber mit dem Befehl !file­ obj gewinnen (beachten Sie, dass hier dieselbe Objektadresse gezeigt wird wie in Process Explorer): lkd> !fileobj ffffc404b6406ef0 Device Object: 0xffffc404b2fa7230 \Driver\KSecDD Vpb is NULL Event signalled Flags: 0x40002 Synchronous IO Handle Created CurrentByteOffset: 0 Da ein Dateiobjekt nur die Darstellung einer gemeinsam nutzbaren Ressource im Arbeitsspeicher ist und nicht die Ressource selbst, unterscheidet es sich von anderen Exekutivobjekten. Ein Dateiobjekt enthält ausschließlich die Daten für das zugehörige Objekthandle, während die Datei selbst die gemeinsam nutzbaren Daten oder Texte einschließt. Jedes Mal, wenn ein Thread eine Datei öffnet, wird ein neues Dateiobjekt mit einem neuen Satz handlespezifischer Attribute erstellt. Betrachten Sie als Beispiel eine Datei, die gleichzeitig mehrmals geöffnet ist. Das Attribut mit dem aktuellen Byteoffset in den einzelnen Dateiobjekten verweist dabei jeweils auf den Speicherort in der Datei, an dem die nächste Lese- oder Schreiboperation über das zugehörige Handle stattfinden wird. Jedes Dateihandle verfügt über seinen eigenen privaten Byteoffset, obwohl die zugrunde liegende Datei dieselbe ist. Ein Dateiobjekt gehört außerdem immer nur zu einem einzigen Prozess. Die einzigen Ausnahmen bestehen, wenn der Prozess das Handle in einen anderen Prozess dupliziert (was mit der Windows-Funktion D­ uplicateHandle möglich ist) oder wenn ein Kindprozess es von seinem Elternprozess erbt. In diesen Fällen haben die beiden Prozesse getrennte Handles, die auf dasselbe Dateiobjekt verweisen. Das Dateihandle gehört zwar nur zu einem einzigen Prozess, nicht aber die zugrunde liegende physische Ressource. Threads müssen daher wie bei allen anderen gemeinsam genutzten Ressourcen ihren Zugriff synchronisieren. Schreibt beispielsweise ein Thread in eine Datei, muss er beim Öffnen dieser Datei exklusiven Schreibzugriff verlangen, damit andere Threads nicht zur gleichen Zeit in die Datei schreiben. Mit der Windows-Funktion LockFile kann der Thread alternativ auch einen Teil der Datei sperren, während er darin schreibt. Gerätetreiber Kapitel 6 585
Beim Öffnen einer Datei wird der Name des Objekts für das Gerät, auf dem sich die Da­ tei befindet, in den Dateinamen eingeschlossen. Beispielsweise bezeichnet der Name \Device\ ​HarddiskVolume1\Myfile.dat die Datei Myfile.dat in dem Volume C:, wobei der Teilstring \Device\HarddiskVolume1 der Name des internen Windows-Geräteobjekts für das Volume ist. Wenn Sie Myfile.dat öffnen, erstellt der E/A-Manager ein Dateiobjekt, speichert darin einen Zeiger auf das Geräteobjekt HarddiskVolume1 und gibt ein Dateihandle an den Aufrufer zurück. Wenn der Aufrufer dieses Handle verwendet, kann der E/A-Manager damit unmittelbar das Geräteobjekt HarddiskVolume1 finden. In Windows-Anwendungen können Sie die internen Windows-Gerätenamen jedoch nicht nutzen. Stattdessen erstellen Gerätetreiber in dem besonderen Verzeichnis \GLOBAL?? im Namensraum des Objekt-Managers symbolische Verknüpfungen zu den internen Windows-­ Gerätenamen, damit ihre Geräte für Windows-Anwendungen zugänglich sind. Diese Verknüpfungen können Sie in Ihren Programmen mithilfe der Windows-Funktionen QueryDosDevice und DefineDosDevice untersuchen bzw. ändern. E/A-Verarbeitung Nachdem Sie jetzt den Aufbau und die Arten von Treibern und die unterstützenden Daten­ strukturen kennen, wollen wir uns den Ablauf von E/A-Anforderungen im System ansehen. Diese Anforderungen durchlaufen mehrere vorhersehbare Verarbeitungsphasen. Welche Phasen das sind, hängt davon ab, ob die Anforderung für ein Gerät bestimmt ist, das von einem einschichtigen oder einem mehrschichtigen Treiber gesteuert wird, und ob der Aufrufer synchrone oder asynchrone E/A verlangt hat. Sehen wir uns daher als Erstes die verschiedenen Arten der E/A an. Verschiedene Arten der E/A Anwendungen können bei ihren E/A-Anforderungen zwischen mehreren Optionen wählen. Außerdem gibt der E/A-Manager Treibern die Möglichkeit, eine E/A-Schnellschnittstelle zu implementieren, mit der sich IRP-Zuweisungen für die E/A-Verarbeitung oft verringern lassen. In diesem Abschnitt sehen wir uns die verschiedenen Optionen für E/A-Anforderungen an. Synchrone und asynchrone E/A Die meisten von Anwendungen angeforderten E/A-Operationen verlaufen synchron (was die Standardvariante ist). Dabei wartet der Anwendungsthread, während das Gerät die Operation ausführt, und gibt einen Statuscode zurück, wenn die E/A abgeschlossen ist. Das Programm kann dann fortfahren und unmittelbar auf die übertragenen Daten zugreifen. In ihrer einfachsten Form werden die Windows-Funktionen ReadFile und WriteFile synchron ausgeführt: Sie schließen die E/A-Operation ab, bevor sie die Steuerung an den Aufrufer zurückgeben. Bei der asynchronen E/A dagegen kann die Anwendung mehrere E/A-Anforderungen ausgeben und fortfahren, während das Gerät die Operation durchführt. Das kann den Durchsatz der Anwendung erhöhen, da sich der Anwendungsthread um andere Arbeiten kümmern kann, während die E/A-Operation abläuft. Um asynchrone E/A nutzen zu können, müssen Sie beim Aufruf der Windows-Funktionen CreateFile und CreateFile2 das Flag FILE_FLAG_OVERLAPPED 586 Kapitel 6 Das E/A-System
angeben. Natürlich darf der Thread nach der Anforderung einer asynchronen E/A-Operation erst dann auf die daraus resultierenden Daten zugreifen, wenn der Gerätetreiber die Operation abgeschlossen hat. Um das zu erreichen, muss er das Handle des Synchronisierungsobjekts überwachen, das beim Abschluss der E/A ausgegeben wird (wobei es sich um ein Ereignisobjekt, einen E/A-Vervollständigungsport oder das Dateiobjekt selbst handeln kann). Unabhängig von der Art der E/A-Anforderung werden E/A-Operationen, die von einem Treiber im Namen einer Anwendung verlangt wurden, asynchron ausgeführt. Das heißt, dass der Gerätetreiber die Steuerung nach dem Auslösen einer E/A-Anforderung so schnell wie möglich an das E/A-System zurückgeben muss. Ob das E/A-System dann die Steuerung sofort wieder an den Aufrufer zurückgibt oder nicht, hängt davon ab, ob das Handle für synchrone oder asynchrone E/A geöffnet wurde. Abb. 6–3 zeigt den Verlauf der Steuerung beim Auslösen einer Leseoperation. Wenn ein Wartevorgang stattfindet – was von der Einstellung des OVERLAPPED-Flags im Dateiobjekt abhängt –, erfolgt er im Kernelmodus durch die Funktion NtReadFile. Den Status einer ausstehenden asynchronen E/A-Operation können Sie mit dem Windows-Makro HasOverlappedIoCompleted in Erfahrung bringen. Nähere Einzelheiten lassen sich auch mit den GetOverlappedResult(Ex)-Funktionen einsehen. Wenn Sie E/A-Vervollständigungsports einsetzen (siehe den entsprechenden Abschnitt weiter hinten in diesem Kapitel), können Sie die GetQueuedCompletionStatus(Ex)-Funktionen nutzen. Schnelle E/A Die schnelle E/A ist ein besonderer Mechanismus, mit dem das E/A-System die Generierung eines IRPs umgehen und sich direkt dem Treiberstack zuwenden kann, um eine E/A-Anforderung abzuschließen. Diese Vorgehensweise dient zur Optimierung bestimmter E/A-Pfade, die bei der Verwendung von IRPs etwas langsamer ablaufen. (Eine ausführlichere Erklärung der schnellen E/A finden Sie in Kapitel 13 und 14 von Band 2.) Um seine Eintrittspunkte für schnelle E/A zu registrieren, gibt ein Treiber sie in eine Struktur ein, auf die der Zeiger PFAST_IO_DISPATCH in seinem Treiberobjekt verweist. Experiment: Die von einem Treiber registrierten Routinen für schnelle E/A einsehen Der Kerneldebuggerbefehl !drvobj kann die Routinen für schnelle E/A auflisten, die ein Treiber in seinem Treiberobjekt registriert. Gewöhnlich haben nur Dateisystemtreiber Verwendung für solche Routinen, doch es gibt auch Ausnahmen wie Netzwerkprotokoll- und Busfiltertreiber. Die folgende Ausgabe zeigt die Tabelle für schnelle E/A des NTFS-Dateisystemtreiberobjekts: lkd> !drvobj \filesystem\ntfs 2 Driver object (ffffc404b2fbf810) is for: \FileSystem\NTFS DriverEntry: fffff80e5663a030 NTFS!GsDriverEntry DriverStartIo: 00000000 → E/A-Verarbeitung Kapitel 6 587
DriverUnload: 00000000 AddDevice: 00000000 Dispatch routines: ... Fast I/O routines: FastIoCheckIfPossible fffff80e565d6750 NTFS!NtfsFastIoCheckIfPossible FastIoRead fffff80e56526430 NTFS!NtfsCopyReadA FastIoWrite fffff80e56523310 NTFS!NtfsCopyWriteA FastIoQueryBasicInfo fffff80e56523140 NTFS!NtfsFastQueryBasicInfo FastIoQueryStandardInfo fffff80e56534d20 NTFS!NtfsFastQueryStdInfo FastIoLock fffff80e5651e610 NTFS!NtfsFastLock FastIoUnlockSingle fffff80e5651e3c0 NTFS!NtfsFastUnlockSingle FastIoUnlockAll fffff80e565d59e0 NTFS!NtfsFastUnlockAll FastIoUnlockAllByKey fffff80e565d5c50 NTFS!NtfsFastUnlockAllByKey ReleaseFileForNtCreateSection fffff80e5644fd90 NTFS!NtfsReleaseForCreate fffff80e56537750 NTFS!NtfsFastQueryNetwork Section FastIoQueryNetworkOpenInfo OpenInfo AcquireForModWrite fffff80e5643e0c0 NTFS!NtfsAcquireFileForModWrite MdlRead fffff80e5651e950 MdlReadComplete fffff802dc6cd844 NTFS!NtfsMdlReadA nt!FsRtlMdlReadCompleteDev PrepareMdlWrite fffff80e56541a10 MdlWriteComplete fffff802dcb76e48 NTFS!NtfsPrepareMdlWriteA nt!FsRtlMdlWriteCompleteDev FastIoQueryOpen fffff80e5653a520 NTFS!NtfsNetworkOpenCreate ReleaseForModWrite fffff80e5643e2c0 NTFS!NtfsReleaseFileForModWrite AcquireForCcFlush fffff80e5644ca60 NTFS!NtfsAcquireFileForCcFlush ReleaseForCcFlush fffff80e56450cf0 NTFS!NtfsReleaseFileForCcFlush Die Ausgabe zeigt, dass NTFS seine Routine NtfsCopyReadA als FastIoRead-Eintrag in der Tabelle für schnelle E/A registriert hat. Wie der Name dieses Eintrags schon andeutet, ruft der E/A-Manager diese Funktion auf, wenn er eine Leseanforderung für eine Datei im Cache ausgibt. Schlägt dieser Aufruf fehl, wird der übliche Weg über IRPs gewählt. 588 Kapitel 6 Das E/A-System
E/A in zugeordneten Dateien und Zwischenspeicherung Die E/A in zugeordneten Dateien ist eine wichtige Funktion, an der das E/A-System und der Speicher-Manager gemeinsam beteiligt sind. (Mehr über zugeordnete Dateien erfahren Sie in Kapitel 5.) Dabei kann eine Datei auf der Festplatte als Teil des virtuellen Speichers eines Prozesses angesehen werden. Ein Programm kann auf die Datei wie auf ein großes Array zugreifen, ohne Daten zu puffern oder E/A auf der Festplatte auszuführen. Das Programm greift dabei auf den Arbeitsspeicher zu, woraufhin der Speicher-Manager mithilfe seines Auslagerungsmechanismus die gewünschte Seite von der Datei auf der Festplatte lädt. Schreibt die Anwendung in ihren virtuellen Adressraum, so schreibt der Speicher-Manager die Änderungen im Rahmen der normalen Auslagerung in die Datei. Mit CreateFileMapping, MapViewOfFile und ähnlichen Windows-Funktionen ist eine E/A in zugeordneten Dateien im Benutzermodus möglich. Innerhalb des Betriebssystems wird diese Vorgehensweise für wichtige Operationen wie die Zwischenspeicherung von Dateien und die Abbildaktivierung (Laden und Ausführen von ausführbaren Programmen) verwendet. Der andere Hauptnutzer der E/A in zugeordneten Dateien ist der Cache-Manager. Dateisysteme nutzen ihn, um Dateidaten im virtuellen Speicher zuzuordnen und damit die Reaktionszeit von E/A-abhängigen Programmen zu verbessern. Während der Aufrufer die Datei nutzt, nimmt der Speicher-Manager die betroffenen Seiten in den Arbeitsspeicher auf. Die meisten Cachingsysteme weisen eine feste Anzahl von Bytes für die Zwischenspeicherung von Dateien im Arbeitsspeicher zu, doch der Windows-Cache wächst und schrumpft je nachdem, wie viel Speicher zur Verfügung steht. Diese Größenanpassung ist möglich, da sich der Cache-Manager darauf verlässt, dass der Speicher-Manager den Cache durch die in Kapitel 5 erklärten Mechanismen für Arbeitssätze automatisch erweitert und verkleinert, wobei diese Mechanismen hier auf den Systemarbeitssatz angewendet werden. Da der Cache-Manager auf das Auslagerungssystem des Speicher-Managers zurückgreift, vermeidet er es, die Arbeit, die der Speicher-Manager ohnehin erledigt, erneut auszuführen. (Die Funktionsweise des Cache-Managers wird ausführlich in Kapitel 14 von Band 2 erklärt.) Scatter-Gather-E/A Über die Funktionen ReadFileScatter und WriteFileGather ermöglicht Windows auch eine Form von Hochleistungs-E/A, die als Scatter-Gather-E/A bezeichnet wird. Mit diesen Funktionen kann eine Anwendung einen einzelnen Lese- oder Schreibvorgang von mehreren Puffern im virtuellen Speicher zu einem zusammenhängenden Bereich einer Datei auf der Festplatte anfordern, ohne für jeden Puffer eine eigene E/A-Anforderung ausgeben zu müssen. Dazu muss die Datei für E/A ohne Caching geöffnet sein, die verwendeten Benutzerpuffer müssen an Seitengrenzen ausgerichtet und die E/As asynchron sein. Erfolgt die E/A in einem Massenspeichergerät, muss sie außerdem an einer Gerätesektorengrenze ausgerichtet sein und eine Länge von einem Vielfachen der Sektorengröße aufweisen. E/A-Verarbeitung Kapitel 6 589
E/A-Anforderungspakete In einem E/A-Anforderungspaket (I/O Request Packet, IRP) speichert das E/A-System die Informationen, die es zur Verarbeitung einer E/A-Anforderung benötigt. Wenn ein Thread eine E/A-API aufruft, erstellt der E/A-Manager ein IRP, das die Operation während ihres Durchlaufs durch das E/A-System repräsentiert. Wenn möglich weist der E/A-Manager IRPs von einer der drei vom Prozessor nicht ausgelagerten Look-Aside-Listen für IRPs zu: QQ Look-Aside-Liste für kleine IRPs Speichert IRPs mit einer Stackposition. (Worum es sich bei diesen Stackpositionen handelt, erklären wir in Kürze.) QQ Look-Aside-Liste für mittlere IRPs Enthält IRPs mit vier Stackpositionen (kann aber auch für IRPs verwendet werden, die nur zwei oder drei Stackpositionen benötigen). QQ Look-Aside-Liste für große IRPs Enthält IRPs mit mehr als vier Stackpositionen. Standardmäßig speichert das System in dieser Liste IRPs mit 14 Stackpositionen, allerdings passt es einmal pro Minute die Anzahl der zugewiesenen Stackpositionen an und kann die Anzahl je nachdem, wie viele Stackpositionen kürzlich erworben wurden, auf bis zu 20 erhöhen. Hinter diesen Listen stehen auch die globalen Look-Aside-Listen, was einen effizienten CPU-übergreifenden IRP-Fluss ermöglicht. Wenn ein IRP mehr Stackpositionen benötigt, als die Liste für große IRPs bietet, weist der E/A-Manager IRPs vom nicht ausgelagerten Pool zu. Zur Zuweisung von IRPs verwendet der E/A-Manager die Funktion IoAllocateIrp, die auch für die Entwickler von Gerätetreibern verfügbar ist, da ein Treiber unter Umständen eine E/A-Anforderung direkt dadurch auslösen muss, dass er seine eigenen IRPs erstellt und initialisiert. Nach der Zuweisung und Initialisierung eines IRPs speichert der E/A-Manager darin einen Zeiger auf das Dateiobjekt des Aufrufers. Falls vorhanden, gibt der DWORD-Wert LargeIrpStackLocations im Registrierungsschlüssel HKLM\System\CurrentControlSet\Session Manager\I/O System an, wie viele Stackpositionen die IRPs auf der Look-Aside-Liste für große IRPs enthalten. Mit dem Wert MediumIrpStackLocations im selben Schlüssel kann die Anzahl der Stackpositionen in der Liste für mittlere IRPs geändert werden. Abb. 6–11 zeigt einige der wichtigen Elemente der IRP-Struktur. Sie wird stets von einem oder mehreren IO_STACK_LOCATION-Objekten begleitet (siehe den nächsten Abschnitt). 590 Kapitel 6 Das E/A-System
MDL MdlAddress MasterIrp AssociatedIrp Aktuelle E/A-Stackposition Zähler der E/A-Stackpositionen IoStatus IrpCount SystemBuffer Benutzerereignis Status Benutzerpuffer Information Abbruchroutine Abbildung 6–11 Wichtige Elemente der IRP-Struktur Diese Elemente haben folgende Bedeutung: QQ IoStatus Dies ist der Status des IRPs. Er besteht selbst aus zwei Elementen, nämlich S­ tatus mit dem eigentlichen Statuscode und dem polymorphen Wert Information, der in einigen Fällen eine Bedeutung hat. Beispielsweise gibt dieser (vom Treiber festgelegte) Wert bei einer Lese- oder Schreiboperation die Anzahl der gelesenen bzw. geschriebenen Bytes an. Derselbe Wert wird von den Funktionen ReadFile und WriteFile als Ausgabewert zurückgegeben. QQ MdlAddress Dies ist ein optionaler Zeiger auf eine Speicherdeskriptorliste (Memory Descriptor List, MDL), also eine Struktur mit Angaben über einen Puffer im physischen Speicher. Die Hauptverwendung von MDLs für Gerätetreiber beschreiben wir im nächsten Abschnitt. Wurde keine MDL angefordert, ist dieser Wert NULL. QQ Zähler der E/A-Stackpositionen und aktuelle Stackposition Hierin sind die Gesamtzahl der nachfolgenden E/A-Stackpositionsobjekte bzw. ein Zeiger auf die aktuelle Position gespeichert, die sich diese Treiberschicht ansehen soll. Im nächsten Abschnitt beschäftigen wir uns eingehender mit Stackpositionen. QQ Benutzerpuffer Dies ist ein Zeiger auf den von dem Client bereitgestellten Puffer, der die E/A-Operation ausgelöst hat, beispielsweise der Puffer für die Funktion ReadFile oder ReadWrite. QQ Benutzerereignis Dies ist das Kernelereignisobjekt, das ggf. bei einer asynchronen E/A-Operation verwendet wurde. Ein Ereignis ist eine Möglichkeit, um über den Abschluss der E/A-Operation zu informieren. E/A-Verarbeitung Kapitel 6 591
QQ Abbruchroutine Dies ist die Funktion, die der E/A-Manager aufruft, wenn das IRP abgebrochen wird. QQ AssociatedIrp Dies ist eine Vereinigung von bis zu drei Feldern: Wenn der E/A-Manager eine gepufferte E/A-Technik verwendet, wird mit SystemBuffer der Benutzerpuffer an den Treiber übergeben. Die gepufferte E/A sowie weitere Möglichkeiten, um Benutzermoduspuffer an Treiber zu übergeben, sind Thema des nächsten Abschnitts. Mit dem Element MasterIrp kann ein Master-IRP erstellt werden, um die Arbeit auf Teil-IRPs aufzuteilen. Der Master wird dabei erst dann als abgeschlossen betrachtet, wenn alle Teil-IRPs abgeschlossen sind. E/A-Stackpositionen Auf ein IRP folgen immer eine oder mehrere E/A-Stackpositionen, wobei die Anzahl dieser Positionen der Anzahl der geschichteten Geräte in dem zugehörigen Geräteknoten entspricht. Die Informationen über die E/A-Operation sind auf den IRP-Rumpf (die Hauptstruktur) und die aktuelle E/A-Stackposition aufgeteilt, wobei aktuell hier die Position bedeutet, die für die betreffende Geräteschicht eingerichtet ist. Abb. 6–12 zeigt die wichtigen Felder einer E/A-­ Stackposition. Beim Erstellen eines IRPs wird die Anzahl der angeforderten Stackpositionen an IoAllocateIrp übergeben. Der E/A-Manager initialisiert dann den IRP-Rumpf und die erste Stackposition, die für das oberste Gerät im Geräteknoten da ist. Jede Schicht im Geräteknoten ist dafür zuständig, die jeweils nächste E/A-Stackposition zu initialisieren, wenn sie den IRP an das nächste Gerät übergibt. Die in Abb. 6–12 gezeigten Elemente haben folgende Bedeutung: QQ Hauptfunktion Dies ist der Hauptcode, der die Art der Anforderung angibt (Lesen, Schreiben, Erstellen, Plug & Play usw.). Er wird auch als Dispatchroutinencode bezeichnet. Dabei handelt es sich um eine von 28 Konstanten (0 bis 27) in wdm.h, die alle mit IRP_MJ_ beginnen. Diesen Index nutzt der E/A-Manager im Funktionszeigerarray MajorFunction, um innerhalb des Treibers zu der benötigten Routine zu springen. In den meisten Treibern sind Dispatchroutinen nur für einen Teil des möglichen Hauptfunktionscodes definiert, z. B. zum Erstellen (Öffnen), Lesen und Schreiben, für die Geräte-E/A-Steuerung, für Energieverwaltung, Plug & Play, Systemsteuerung (für WMI-Befehle) und Bereinigung sowie zum Schließen. Dateisystemtreiber füllen gewöhnlich die meisten oder alle ihrer Dispatcheintrittspunkte mit Funktionen aus. Dagegen sind in einem Treiber für ein einfaches USB-­ Gerät meistens nur die Routinen zum Öffnen, Schließen, Lesen und Schreiben und zum Senden von E/A-Steuercodes angegeben. Der E/A-Manager sorgt dafür, dass die vom Treiber nicht ausgefüllten Dispatcheintrittspunkte auf seine eigene Funktion IopInvalid­ DeviceRequest zeigen. Sie schließt das IRP mit einem Fehlerstatus ab, der angibt, dass die im IRP angegebene Hauptfunktion für das Gerät ungültig ist. QQ Nebenfunktion Dies dient bei einigen Funktionen zur Erweiterung des Hauptfunktionscodes. So haben zwar IRP_MJ_READ (Lesen) und IRP_MJ_WRITE (Schreiben) keine Nebenfunktionen, während die IRPs für Plug & Play und Energieverwaltung immer einen Nebenfunktionscode zur weiteren Spezialisierung des Hauptcodes enthalten. Der Hauptfunktionscode 592 Kapitel 6 Das E/A-System
IRP_MJ_PNP für Plug & Play ist so allgemein, dass die eigentliche Anweisung im Nebencode angegeben wird, z. B. IRP_MN_START_DEVICE, IRP_MN_REMOVE_DEVICE usw. QQ Parameter Dies ist eine riesige Vereinigung von Strukturen, die jeweils für einen bestimmten Hauptfunktionscode oder eine Kombination aus Haupt- und Nebencode gültig sind. Beispielsweise enthält die Struktur Parameters.Read für eine Leseoperation (IRP_MJ_ READ) Informationen über die Leseanforderung, darunter die Puffergröße. QQ FileObject und DeviceObject Diese Elemente zeigen auf die FILE_OBJECT- bzw. DEVICE_ OBJECT-Struktur für die E/A-Anforderung. QQ CompletionRoutine Dies ist die optionale Vervollständigungsfunktion, die ein Treiber in der DDI IoSetCompletionRoutine(Ex) registrieren kann und die aufgerufen wird, wenn das IRP von einem Treiber einer niedrigeren Schicht abgeschlossen wird. An dieser Stelle kann sich der Treiber den Abschlussstatus des IRPs ansehen und jegliche noch erforderliche abschließende Verarbeitung durchführen. Er kann den Abschluss sogar rückgängig machen (indem er in der Funktion den besonderen Wert STATUS_MORE_PROCESSING_REQUIRED zurückgibt) und das IRP erneut an den Geräteknoten senden, auch mit geänderten Parametern oder an einen anderen Geräteknoten. QQ Kontext Dies ist ein willkürlicher Wert, der mit IoSetCompletionRoutine(Ex) festgelegt und so wie er ist an die Vervollständigungsroutine übergeben wird. Hauptfunktion Nebenfunktion Parameter FileObject DeviceObject CompletionRoutine Kontext Abbildung 6–12 Wichtige Elemente der IO_STACK_LOCATION-Struktur Die Aufteilung der Informationen auf den IRP-Rumpf und die E/A-Stackposition macht es möglich, die Parameter in der Stackposition für das nächste Gerät im Gerätestack zu ändern und gleichzeitig die ursprünglichen Anforderungsparameter beizubehalten. Beispielsweise wird ein Lese-IRP für ein USB-Gerät vom Funktionstreiber oft in ein Gerätesteuerungs-IRP geändert, bei dem das Eingabepufferargument der Gerätesteuerung auf einen USB-Anforderungsblock (URB) zeigt, das von dem USB-Bustreiber der niedrigeren Schicht verstanden wird. Außerdem kann jede Schicht (außer der untersten) ihre eigene Vervollständigungsroutine registrieren, wobei jede in der darunter liegenden Stackposition gespeichert wird. E/A-Verarbeitung Kapitel 6 593
Experiment: Dispatchroutinen von Treibern untersuchen Um sich eine Liste der Funktionen anzeigen zu lassen, die ein Treiber für seine Dispatchroutinen definiert hat, verwenden Sie den Kerneldebuggerbefehl !drvobj mit Bit 1 (Wert 2). Die folgende Ausgabe zeigt die Hauptfunktionscodes der vom NTFS-Treiber unterstützten Funktionscodes. (Dies ist dasselbe Experiment wie bei der schnellen E/A.) lkd> !drvobj \filesystem\ntfs 2 Driver object (ffffc404b2fbf810) is for: \FileSystem\NTFS DriverEntry: fffff80e5663a030 NTFS!GsDriverEntry DriverStartIo: 00000000 DriverUnload: 00000000 AddDevice: 00000000 Dispatch routines: [00] IRP_MJ_CREATE fffff80e565278e0 NTFS!NtfsFsdCreate [01] IRP_MJ_CREATE_NAMED_PIPE fffff802dc762c80 nt!IopInvalidDeviceRequest [02] IRP_MJ_CLOSE fffff80e565258c0 NTFS!NtfsFsdClose [03] IRP_MJ_READ fffff80e56436060 NTFS!NtfsFsdRead [04] IRP_MJ_WRITE fffff80e564461d0 NTFS!NtfsFsdWrite [05] IRP_MJ_QUERY_INFORMATION fffff80e565275f0 NTFS!NtfsFsdDispatchWait [06] IRP_MJ_SET_INFORMATION fffff80e564edb80 NTFS!NtfsFsdSetInformation [07] IRP_MJ_QUERY_EA fffff80e565275f0 NTFS!NtfsFsdDispatchWait [08] IRP_MJ_SET_EA fffff80e565275f0 NTFS!NtfsFsdDispatchWait [09] IRP_MJ_FLUSH_BUFFERS fffff80e5653c9a0 NTFS!NtfsFsdFlushBuffers [0a] IRP_MJ_QUERY_VOLUME_INFORMATION fffff80e56538d10 NTFS!NtfsFsdDispatch [0b] IRP_MJ_SET_VOLUME_INFORMATION [0c] IRP_MJ_DIRECTORY_CONTROL fffff80e56538d10 NTFS!NtfsFsdDispatch fffff80e564d7080 NTFS!NtfsFsdDirectoryControl [0d] IRP_MJ_FILE_SYSTEM_CONTROL fffff80e56524b20 NTFS!NtfsFsdFileSystemControl [0e] IRP_MJ_DEVICE_CONTROL fffff80e564f9de0 NTFS!NtfsFsdDeviceControl [0f] IRP_MJ_INTERNAL_DEVICE_CONTROL fffff802dc762c80 nt!IopInvalidDeviceRequest [10] IRP_MJ_SHUTDOWN fffff80e565efb50 NTFS!NtfsFsdShutdown [11] IRP_MJ_LOCK_CONTROL fffff80e5646c870 NTFS!NtfsFsdLockControl [12] IRP_MJ_CLEANUP fffff80e56525580 NTFS!NtfsFsdCleanup [13] IRP_MJ_CREATE_MAILSLOT fffff802dc762c80 nt!IopInvalidDeviceRequest [14] IRP_MJ_QUERY_SECURITY fffff80e56538d10 NTFS!NtfsFsdDispatch [15] IRP_MJ_SET_SECURITY fffff80e56538d10 NTFS!NtfsFsdDispatch [16] IRP_MJ_POWER fffff802dc762c80 nt!IopInvalidDeviceRequest [17] IRP_MJ_SYSTEM_CONTROL fffff802dc762c80 nt!IopInvalidDeviceRequest → 594 Kapitel 6 Das E/A-System
[18] IRP_MJ_DEVICE_CHANGE fffff802dc762c80 nt!IopInvalidDeviceRequest [19] IRP_MJ_QUERY_QUOTA fffff80e565275f0 NTFS!NtfsFsdDispatchWait [1a] IRP_MJ_SET_QUOTA fffff80e565275f0 NTFS!NtfsFsdDispatchWait [1b] IRP_MJ_PNP fffff80e56566230 NTFS!NtfsFsdPnp Fast I/O routines: ... Wenn ein IRP aktiv ist, wird es gewöhnlich in die IRP-Liste des Threads gestellt, der die E/A angefordert hat. (Bei der in einem eigenen Abschnitt weiter hinten in diesem Kapitel beschriebenen threadagnostischen E/A wird es im Dateiobjekt gespeichert.) Dadurch kann das E/A-System ausstehende IRPs finden und abbrechen, wenn ein Thread mit nicht abgeschlossenen IRPs beendet wird. IRPs für Auslagerungs-E/A sind außerdem mit dem Fehlerthread verknüpft (obwohl sie nicht abgebrochen werden können). Dadurch kann Windows die Optimierung der threadagnostischen E/A anwenden, wenn der aktuelle Thread der auslösende Thread ist und zur Vervollständigung der E/A kein asynchroner Prozeduraufruf (APC) eingesetzt wird. Das bedeutet, dass Seitenfehler inline auftreten und keine APC-Auslieferung erfordern. Experiment: Die ausstehenden IRPs eines Threads anzeigen Der Befehl !thread gibt alle mit einem Thread verknüpften IRPs aus. Auf entsprechende Anweisung macht der Befehl !process das auch. Führen Sie im Kerneldebugger ein lokales oder ein Live-Debugging durch und lassen Sie sich dabei die Threads eines Explorerprozesses anzeigen: lkd> !process 0 7 explorer.exe PROCESS ffffc404b673c780 SessionId: 1 Cid: 10b0 Peb: 00cbb000 ParentCid: 1038 DirBase: 8895f000 ObjectTable: ffffe689011b71c0 HandleCount: <Data Not Accessible> Image: explorer.exe VadRoot ffffc404b672b980 Vads 569 Clone 0 Private 7260. Modified 366527. Locked 784. DeviceMap ffffe688fd7a5d30 Token ffffe68900024920 ElapsedTime 18:48:28.375 UserTime 00:00:17.500 KernelTime 00:00:13.484 ... MemoryPriority BACKGROUND BasePriority 8 CommitCharge 10789 Job ffffc404b6075060 → E/A-Verarbeitung Kapitel 6 595
THREAD ffffc404b673a080 Cid 10b0.10b4 Teb: 0000000000cbc000 Win32Thread: ffffc404b66e7090 WAIT: (WrUserRequest) UserMode Non-Alertable ffffc404b6760740 SynchronizationEvent Not impersonating ... THREAD ffffc404b613c7c0 Cid 153c.15a8 Teb: 00000000006a3000 Win32Thread: ffffc404b6a83910 WAIT: (UserRequest) UserMode Non-Alertable ffffc404b58d0d60 SynchronizationEvent ffffc404b566f310 SynchronizationEvent IRP List: ffffc404b69ad920: (0006,02c8) Flags: 00060800 Mdl: 00000000 ... In der Ausgabe sehen Sie eine Menge von Threads. Im Abschnitt IRP List der Threadinformationen werden bei den meisten dieser Threads die IRPs angezeigt. (Beachten Sie, dass der Debugger bei einem Thread mit mehr als 17 ausstehenden E/A-Anforderungen nur die ersten 17 IRPs ausgibt.) Wählen Sie ein IRP aus und untersuchen Sie es mit dem Befehl !irp: lkd> !irp ffffc404b69ad920 Irp is active with 2 stacks 1 is current (= 0xffffc404b69ad9f0) No Mdl: No System Buffer: Thread ffffc404b613c7c0: Irp stack trace. cmd flg cl Device File Completion-Context >[IRP_MJ_FILE_SYSTEM_CONTROL(d), N/A(0)] 5 e1 ffffc404b253cc90 ffffc404b5685620 fffff80e55752ed0-ffffc404b63c0e00 Success Error Cancel pending \FileSystem\Npfs FLTMGR!FltpPassThroughCompletion Args: 00000000 00000000 00110008 00000000 [IRP_MJ_FILE_SYSTEM_CONTROL(d), N/A(0)] 5 0 ffffc404b3cdca00 ffffc404b5685620 00000000-00000000 \FileSystem\FltMgr Args: 00000000 00000000 00110008 00000000 Das IRP hat zwei Stackpositionen. Sein Ziel ist ein Gerät, das dem NPFS-Treiber gehört (Named Pipe File System; siehe Kapitel 10 in Band 2). 596 Kapitel 6 Das E/A-System
IRP-Fluss IRPs werden gewöhnlich vom E/A-Manager erstellt und dann an das erste Gerät des Zielgeräteknotens gesendet. Abb. 6–13 zeigt einen typischen IRP-Fluss für Hardwaregerätetreiber. PnP-Manager Verarbeitung auf dem Weg nach unten E/A-Manager Energieverwaltung Verarbeitung auf dem Weg nach oben Vervollständigungs­ routine aufrufen Vervollständigungsroutine registrieren Vervollständigungs­ routine aufrufen Vervollständigungsroutine registrieren Vollständige Anforderung Abbildung 6–13 IRP-Fluss Allerdings werden IRPs nicht nur vom E/A-Manager, sondern auch vom PnP-Manager und von der Energieverwaltung angelegt, wobei der Hauptfunktionscode IRP_MJ_PNP bzw. IRP_MJ_POWER angegeben wird. Abb. 6–13 zeigt einen Geräteknoten mit sechs geschichteten Geräteobjekten: zwei oberen Filtern, dem FDO, zwei unteren Filtern und dem PDO. Ein IRP für diesen Knoten wird daher mit sechs E/A-Stackpositionen erstellt, einer für jede Schicht. IRPs werden immer zu dem Gerät der höchsten Schicht ausgeliefert, auch wenn ein Handle zu einem benannten Gerät tiefer in dem Stack geöffnet wurde. Wenn ein Treiber ein IRP empfängt, kann er eine der folgenden Aktionen ausführen: QQ Er kann das IRP sofort abschließen, indem er IoCompleteRequest aufruft. Das kann vorkommen, wenn das IRP ungültige Parameter enthält (etwa eine unzureichende Puffergröße oder einen nicht erkannten E/A-Steuercode) oder wenn es sich bei der angeforderten Operation um eine handelt, die sofort erledigt werden kann, z. B. um den Abruf eines Gerätestatus oder das Lesen eines Registrierungswerts. Der Treiber ruft IoGetCurrentIrpStackLocation auf, um einen Zeiger auf die Stackposition zu erhalten, an die er sich wenden muss. QQ Der Treiber kann das IRP an die nächste Schicht weiterleiten (und zuvor auch optional eine gewisse Verarbeitung durchführen). Beispielsweise kann ein oberer Filter einige Protokollierungsaufgaben ausführen und das IRP dann zur eigentlichen Ausführung weiter nach unten leiten. Vor dieser Übergabe muss er jedoch die nächste E/A-Stapelposition vorbereiten, die der nächste Treiber in der Reihe verwenden soll. Wenn er keine Änderungen E/A-Verarbeitung Kapitel 6 597
vornehmen will, kann er das Makro IoSkipCurrentIprStackLocation nutzen. Anderenfalls kann er mit IoCopyIrpStackLocationToNext eine Kopie der Stackposition erstellen und sie ändern, indem er mit IoGetNextIrpStackLocation einen Zeiger darauf abruft. Nachdem er die nächste E/A-Stackposition vorbereitet hat, ruft der Treiber IoCallDriver auf, um das IRP weiterzuleiten. QQ Als Erweiterung der vorstehenden Möglichkeit kann der Treiber auch eine Vervollständigungsroutine registrieren, indem er vor der Weitergabe des IRPs IoSetCompletionRoutine(Ex) aufruft. Jede Schicht außer der untersten kann eine solche Vervollständigungsroutine registrieren. (In der untersten Schicht wäre das sinnlos, da dieser Treiber das IRP abschließen muss, weshalb kein Callback erforderlich ist.) Nachdem ein Treiber einer tieferen Schicht IoCompleteRequest aufgerufen hat, wandert das IRP wieder nach oben (siehe Abb. 6–13), wobei die vorhandenen Vervollständigungsroutinen in umgekehrter Reihenfolge der Registrierung aufgerufen werden. Der Ursprung des IRPs (E/A-Manager, PnP-Manager oder Energieverwaltung) verwendet diesen Mechanismus, um eine abschließende Verarbeitung am IRP durchzuführen und es schließlich freizugeben. Da die Anzahl der Geräte auf einem Stack im Voraus bekannt ist, weist der E/A-Manager für jeden beteiligten Gerätetreiber eine Stackposition zu. Es gibt jedoch Situationen, in denen ein IRP zu einem neuen Treiberstack weitergeleitet werden kann. Dies kann etwa bei der Verwendung des Filter-Managers geschehen, der es einem Filter ermöglicht, ein IRP zu einem anderen Filter umzuleiten (z. B. von einem lokalen zu einem Netzwerkdateisystem). Der E/A-Manager stellt die API IoAdjustStackSizeForRedirection bereit, die diese Vorgehensweise ermöglicht, indem sie die erforderlichen Stackpositionen anhand der Geräte auf dem Umleitungsstack hinzufügt. Experiment: Einen Gerätestack anzeigen Der Kerneldebuggerbefehl !devstack zeigt den Gerätestack der geschichteten Geräteobjekte für das angegebene Geräteobjekt an. In dem folgenden Beispiel sehen Sie den Stack für das Geräteobjekt \device\keyboardclass0, das dem Tastatur-Klassentreiber gehört: lkd> !devstack keyboardclass0 !DevObj !DrvObj !DevExt ObjectName \Driver\kbdclass ffff9c80c0424590 KeyboardClass0 ffff9c80c04247c0 \Driver\kbdhid ffff9c80c0424910 ffff9c80c0414060 \Driver\mshidkmdf ffff9c80c04141b0 > ffff9c80c0424440 0000003f !DevNode ffff9c80c0414d30 : DeviceInst is "HID\MSHW0029&Col01\5&1599b1c7&0&0000" ServiceName is "kbdhid" Das Zeichen > in der ersten Spalte der Ausgabe hebt den Eintrag für KeyboardClass0 hervor. Die Einträge oberhalb dieser Zeile sind Treiber oberhalb dieser Schicht, die Zeilen darunter Treiber unterhalb der Schicht. 598 Kapitel 6 Das E/A-System
Experiment: IRPs untersuchen In diesem Experiment suchen Sie nach einem nicht abgeschlossenen IRP auf dem System und bestimmen des1sen Typ, das Zielgerät, den Treiber, der das Gerät verwaltet, den Thread, der das IRP ausgegeben hat, und den Prozess, zu dem dieser Thread gehört. Am besten führen Sie dieses Experiment auf einem 32-Bit-System mit nicht lokalem Kerneldebugging durch. Es funktioniert zwar auch mit lokalem Kerneldebugging, aber dabei kann es sein, dass die IRPs in der Zeit zwischen der Ausgabe der einzelnen Befehle abgeschlossen werden, sodass Sie mit uneinheitlichen Daten rechnen müssen. Auf einem System gibt es immer einige nicht abgeschlossene IRPs, da es viele Geräte gibt, für die Anwendungen IRPs ausgeben können, und die Treiber die IRPs erst dann abschließen, wenn ein bestimmtes Ereignis eintritt, z. B. wenn die Daten zur Verfügung stehen. Ein Beispiel dafür ist ein blockierender Lesevorgang von einem Netzwerkendpunkt. Mit dem Kerneldebuggerbefehl !irpfind können Sie sich die ausstehenden IRPs auf einem System ansehen. (Dieser Befehl kann einige Zeit in Anspruch nehmen. Sie können ihn aber anhalten, sobald einige IRPs erschienen sind.) kd> !irpfind Scanning large pool allocation table for tag 0x3f707249 (Irp?) (a5000000 : a5200000) Irp [ Thread ] irpStack: (Mj,Mn) DevObj [Driver] MDL Process 9515ad68 [aa0c04c0] irpStack: ( e, 5) 8bcb2ca0 [ \Driver\AFD] 0xaa1a3540 8bd5c548 [91deeb80] irpStack: ( e,20) 8bcb2ca0 [ \Driver\AFD] 0x91da5c40 Searching nonpaged pool (80000000 : ffc00000) for tag 0x3f707249 (Irp?) 86264a20 [86262040] irpStack: ( e, 0) 8a7b4ef0 [ \Driver\vmbus] 86278720 [91d96b80] irpStack: ( e,20) 8bcb2ca0 [ \Driver\AFD] 0x86270040 86279e48 [91d96b80] irpStack: ( e,20) 8bcb2ca0 [ \Driver\AFD] 0x86270040 862a1868 [862978c0] irpStack: ( d, 0) 8bca4030 [ \FileSystem\Npfs] 862a24c0 [86297040] irpStack: ( d, 0) 8bca4030 [ \FileSystem\Npfs] 862c3218 [9c25f740] irpStack: ( c, 2) 8b127018 [ \FileSystem\NTFS] 862c4988 [a14bf800] irpStack: ( e, 5) 8bcb2ca0 [ \Driver\AFD] 0xaa1a3540 862c57d8 [a8ef84c0] irpStack: ( d, 0) 8b127018 [ \FileSystem\NTFS] 0xa8e6f040 862c91c0 [99ac9040] irpStack: ( 3, 0) 8a7ace48 [ \Driver\vmbus] 0x9517ac40 862d2d98 [9fd456c0] irpStack: ( e, 5) 8bcb2ca0 [ \Driver\AFD] 0x9fc11780 862d6528 [9aded800] irpStack: ( c, 2) 8b127018 [ \FileSystem\NTFS] 862e3230 [00000000] Irp is complete (CurrentLocation 2 > StackCount 1) 862ec248 [862e2040] irpStack: ( d, 0) 8bca4030 [ \FileSystem\Npfs] 862f7d70 [91dd0800] irpStack: ( d, 0) 8bca4030 [ \FileSystem\Npfs] 863011f8 [00000000] Irp is complete (CurrentLocation 2 > StackCount 1) → E/A-Verarbeitung Kapitel 6 599
86327008 [00000000] Irp is complete (CurrentLocation 43 > StackCount 42) 86328008 [00000000] Irp is complete (CurrentLocation 43 > StackCount 42) 86328960 [00000000] Irp is complete (CurrentLocation 43 > StackCount 42) 86329008 [00000000] Irp is complete (CurrentLocation 43 > StackCount 42) 863296d8 [00000000] Irp is complete (CurrentLocation 2 > StackCount 1) 86329960 [00000000] Irp is complete (CurrentLocation 43 > StackCount 42) 89feeae0 [00000000] irpStack: ( e, 0) 8a765030 [ \Driver\ACPI] 8a6d85d8 [99aa1040] irpStack: ( d, 0) 8b127018 [ \FileSystem\NTFS] 0x00000000 8a6dc828 [8bc758c0] irpStack: ( 4, 0) 8b127018 [ \FileSystem\NTFS] 0x00000000 8a6f42d8 [8bc728c0] irpStack: ( 4,34) 8b0b8030 [ \Driver\disk] 0x00000000 8a6f4d28 [8632e6c0] irpStack: ( 4,34) 8b0b8030 [ \Driver\disk] 0x00000000 8a767d98 [00000000] Irp is complete (CurrentLocation 6 > StackCount 5) 8a788d98 [00000000] irpStack: ( f, 0) 00000000 [00000000: Could not read device object or _DEVICE_OBJECT not found ] 8a7911a8 [9fdb4040] irpStack: ( e, 0) 86325768 [ \Driver\DeviceApi] 8b03c3f8 [00000000] Irp is complete (CurrentLocation 2 > StackCount 1) 8b0b8bc8 [863d6040] irpStack: ( e, 0) 8a78f030 [ \Driver\vmbus] 8b0c48c0 [91da8040] irpStack: ( e, 5) 8bcb2ca0 [ \Driver\AFD] 0xaa1a3540 8b118d98 [00000000] Irp is complete (CurrentLocation 9 > StackCount 8) 8b1263b8 [00000000] Irp is complete (CurrentLocation 8 > StackCount 7) 8b174008 [aa0aab80] irpStack: ( 4, 0) 8b127018 [ \FileSystem\NTFS] 0xa15e1c40 8b194008 [aa0aab80] irpStack: ( 4, 0) 8b127018 [ \FileSystem\NTFS] 0xa15e1c40 8b196370 [8b131880] irpStack: ( e,31) 8bcb2ca0 [ \Driver\AFD] 8b1a8470 [00000000] Irp is complete (CurrentLocation 2 > StackCount 1) 8b1b3510 [9fcd1040] irpStack: ( e, 0) 86325768 [ \Driver\DeviceApi] 8b1b35b0 [a4009b80] irpStack: ( e, 0) 86325768 [ \Driver\DeviceApi] 8b1cd188 [9c3be040] irpStack: ( e, 0) 8bc73648 [ \Driver\Beep] ... Einige IRPs sind abgeschlossen, und bei anderen kann die Zuweisung bald aufgehoben werden. Es kann auch sein, dass die Zuweisung schon aufgehoben ist, das IRP aber noch nicht durch ein neues ersetzt wurde, da die Zuweisung von einer Look-Aside-Liste stammte. Für jedes IRP wird die Adresse gefolgt von dem Thread angegeben, der die Anforderung ausgegeben hat. Darauf folgen der Haupt- und der Nebenfunktionscode der aktuellen Stackposition in Klammern. Um ein IRP zu untersuchen, verwenden Sie den Befehl !irp: kd> !irp 8a6f4d28 Irp is active with 15 stacks 6 is current (= 0x8a6f4e4c) Mdl=8b14b250: No System Buffer: Thread 8632e6c0: Irp stack trace. cmd flg cl Device File Completion-Context [N/A(0), N/A(0)] 0 0 00000000 00000000 00000000-00000000 → 600 Kapitel 6 Das E/A-System
Args: 00000000 00000000 00000000 00000000 [N/A(0), N/A(0)] 0 0 00000000 00000000 00000000-00000000 Args: 00000000 00000000 00000000 00000000 [N/A(0), N/A(0)] 0 0 00000000 00000000 00000000-00000000 Args: 00000000 00000000 00000000 00000000 [N/A(0), N/A(0)] 0 0 00000000 00000000 00000000-00000000 Args: 00000000 00000000 00000000 00000000 [N/A(0), N/A(0)] 0 0 00000000 00000000 00000000-00000000 Args: 00000000 00000000 00000000 00000000 >[IRP_MJ_WRITE(4), N/A(34)] 14 e0 8b0b8030 00000000 876c2ef0-00000000 Success Error Cancel \Driver\disk partmgr!PmIoCompletion Args: 0004b000 00000000 4b3a0000 00000002 [IRP_MJ_WRITE(4), N/A(3)] 14 e0 8b0fc058 00000000 876c36a0-00000000 Success Error Cancel \Driver\partmgr partmgr!PartitionIoCompletion Args: 4b49ace4 00000000 4b3a0000 00000002 [IRP_MJ_WRITE(4), N/A(0)] 14 e0 8b121498 00000000 87531110-8b121a30 Success Error Cancel \Driver\partmgr volmgr!VmpReadWriteCompletionRoutine Args: 0004b000 00000000 2bea0000 00000002 [IRP_MJ_WRITE(4), N/A(0)] 4 e0 8b121978 00000000 82d103e0-8b1220d9 Success Error Cancel \Driver\volmgr fvevol!FvePassThroughCompletionRdpLevel2 Args: 0004b000 00000000 4b49acdf 00000000 [IRP_MJ_WRITE(4), N/A(0)] 4 e0 8b122020 00000000 82801a40-00000000 Success Error Cancel \Driver\fvevol rdyboost!SmdReadWriteCompletion Args: 0004b000 00000000 2bea0000 00000002 [IRP_MJ_WRITE(4), N/A(0)] 4 e1 8b118538 00000000 828637d0-00000000 Success Error Cancel pending \Driver\rdyboost iorate!IoRateReadWriteCompletion Args: 0004b000 3fffffff 2bea0000 00000002 [IRP_MJ_WRITE(4), N/A(0)] 4 e0 8b11ab80 00000000 82da1610-8b1240d8 Success Error Cancel \Driver\iorate volsnap!VspRefCountCompletionRoutine Args: 0004b000 00000000 2bea0000 00000002 → E/A-Verarbeitung Kapitel 6 601
[IRP_MJ_WRITE(4), N/A(0)] 4 e1 8b124020 00000000 87886ada-89aec208 Success Error Cancel pending \Driver\volsnap NTFS!NtfsMasterIrpSyncCompletionRoutine Args: 0004b000 00000000 2bea0000 00000002 [IRP_MJ_WRITE(4), N/A(0)] 4 e0 8b127018 a6de4bb8 871227b2-9ef8eba8 Success Error Cancel \FileSystem\NTFS FLTMGR!FltpPassThroughCompletion Args: 0004b000 00000000 00034000 00000000 [IRP_MJ_WRITE(4), N/A(0)] 4 1 8b12a3a0 a6de4bb8 00000000-00000000 pending \FileSystem\FltMgr Args: 0004b000 00000000 00034000 00000000 Irp Extension present at 0x8a6f4fb4: Dieses Monster-IRP verfügt über 15 Stackpositionen, wobei die fett hervorgehobene Position und im Debugger mit > gekennzeichnete Position 6 die aktuelle ist. Für jede Stackposition sind die Haupt- und Nebenfunktion sowie Adressen des Geräteobjekts und der Vervollständigungsroutinen angegeben. Der nächste Schritt besteht darin, den Befehl !devobj auf die Geräteobjektadresse in der aktuellen Stackposition anzuwenden, um das Zielobjekt des IRP zu ermitteln: kd> !devobj 8b0b8030 Device object (8b0b8030) is for: DR0 \Driver\disk DriverObject 8b0a7e30 Current Irp 00000000 RefCount 1 Type 00000007 Flags 01000050 Vpb 8b0fc420 SecurityDescriptor 87da1b58 DevExt 8b0b80e8 DevObjExt 8b0b8578 Dope 8b0fc3d0 ExtensionFlags (0x00000800) DOE_DEFAULT_SD_PRESENT Characteristics (0x00000100) FILE_DEVICE_SECURE_OPEN AttachedDevice (Upper) 8b0fc058 \Driver\partmgr AttachedTo (Lower) 8b0a4d10 \Driver\storflt Device queue is not busy. Schließlich können Sie sich mit !thread Angaben zu dem Thread und dem Prozess ansehen, die das IRP ausgegeben haben: kd> !thread 8632e6c0 THREAD 8632e6c0 Cid 0004.0058 Teb: 00000000 Win32Thread: 00000000 WAIT: (Executive) KernelMode Non-Alertable 89aec20c NotificationEvent IRP List: 8a6f4d28: (0006,02d4) Flags: 00060043 Mdl: 8b14b250 → 602 Kapitel 6 Das E/A-System
Not impersonating DeviceMap 87c025b0 Owning Process 86264280 Image: System Attached Process N/A Image: N/A Wait Start TickCount 8083 Ticks: 1 (0:00:00:00.015) Context Switch Count 2223 IdealProcessor: 0 UserTime 00:00:00.000 KernelTime 00:00:00.046 Win32 Start Address nt!ExpWorkerThread (0x81e68710) Stack Init 89aecca0 Current 89aebeb4 Base 89aed000 Limit 89aea000 Call 00000000 Priority 13 BasePriority 13 PriorityDecrement 0 IoPriority 2 PagePriority 5 E/A-Anforderungen an einen einschichtigen Hardwaretreiber In diesem Abschnitt sehen wir uns E/A-Anforderungen an einen einschichtigen Kernelmodus-Gerätetreiber genauer an. Abb. 6–14 zeigt den typischen Verlauf der IRP-Verarbeitung für einen solchen Treiber. Benutzermodus Aufruf von z. B. ReadFile durch die App Kernelmodus Validierung der Anforderung Kontext des anfordernden Threads Dispatchroutine Kontext eines beliebigen Threads E/A-Startroutine Geräteinterrupt Softwareinterrupt (DPC) ISR DPC-Routine Kontext eines beliebigen Threads Kontext des anfordernden Threads Besonderer Kernel-APC Abbildung 6–14 Typische einschichtige Verarbeitung von E/A-Anforderungen für Hardwaretreiber Bevor wir uns die einzelnen Schritte in Abb. 6–14 genauer ansehen, sind einige allgemeine Hinweise angebracht: QQ In der Abbildung sind zwei Arten von horizontalen Trennlinien zu sehen. Die erste, durchgezogene Linie ist die übliche Trennung zwischen Benutzer- und Kernelmodus. Die gestri- E/A-Verarbeitung Kapitel 6 603
chelten Linien dagegen setzen den Code, der im Kontext des anfordernden Threads läuft, von dem Code im Kontext eines beliebigen Threads ab. Definiert sind diese Kontexte wie folgt: • In dem Abschnitt, der zum Kontext des anfordernden Threads gehört, ist der ausführende Thread derjenige, der ursprünglich die E/A-Operation angefordert hat. Das ist wichtig, denn das bedeutet, dass auch der Prozesskontext derjenige des ursprünglichen Prozesses ist, weshalb der Benutzermodus-Adressraum, der den an die E/A-Operation übergebenen Benutzerpuffer enthält, direkt zugänglich ist. • In dem Abschnitt, der zum Kontext eines beliebigen Threads gehört, ist der Thread sehr wahrscheinlich nicht der anfordernde Thread, weshalb der sichtbare Benutzermodus-Prozessadressraum sehr wahrscheinlich nicht der des ursprünglichen Prozesses ist. In diesem Kontext kann der Versuch, mit einer Benutzermodusadresse auf den Benutzerpuffer zuzugreifen, katastrophale Folgen haben. Im nächsten Abschnitt erfahren Sie, wie dieses Problem gehandhabt wird. Die Erklärungen für die in Abb. 6–14 gezeigten Schritte machen deutlich, warum sich die Trennlinien an den angegebenen Stellen befinden. QQ Das große Viereck um die vier Blöcke Dispatchroutine, E/A-Startroutine, ISR und DPC-Routine kennzeichnet den vom Treiber bereitgestellten Code. Alle anderen Blöcke werden vom System bereitgestellt. QQ In der Abbildung ist vorausgesetzt, dass das Hardwaregerät nur eine Operation auf einmal handhaben kann, was bei vielen Arten von Geräten der Fall ist. Selbst wenn das Gerät mit mehreren Anforderungen umgehen kann, bleibt der grundlegende Verlauf der Operation doch identisch. Wie in Abb. 6–14 gezeigt, laufen die Ereignisse wie folgt ab: 1. Eine Clientanwendung ruft eine Windows-API wie ReadFile auf, die ihrerseits die native Funktion NtReadFile (in Ntdll.dll) aufruft. Diese wiederum überführt den Thread in den Kernelmodus zu der Exekutivfunktion NtReadFile. (Diese Schritte wurden bereits weiter vorn in diesem Kapitel beschrieben.) 2. Der E/A-Manager in der Implementierung von NtReadFile führt einige Sicherheitsprüfungen an der Anforderung durch, z. B. ob der vom Client bereitgestellte Puffer mit dem richtigen Seitenschutz zugänglich ist. Anschließend sucht der E/A-Manager den zugehörigen Treiber (mithilfe des bereitgestellten Dateihandles), weist ein IRP zu und initialisiert es und ruft den Treiber in der passenden Dispatchroutine auf (in diesem Fall in der Routine, die dem Index IRP_MJ_READ entspricht), indem er IoCallDriver mit dem IRP verwendet. 3. Jetzt sieht der Treiber das IRP zum ersten Mal. Der Aufruf erfolgt gewöhnlich über den aufrufenden Thread. Die einzige Möglichkeit, um das zu verhindern, besteht darin, einen oberen Filter auf dem IRP einzusetzen und IoCallDriver später von einem anderen Thread aus aufzurufen. Wir gehen hier jedoch davon aus, dass das nicht der Fall ist. (Bei den meisten Hardwaregeräten geschieht das auch tatsächlich nicht. Selbst wenn es obere 604 Kapitel 6 Das E/A-System
Filter gibt, so führen sie lediglich eine gewisse Verarbeitung aus und rufen dann sofort den unteren Treiber aus demselben Thread heraus auf.) Der Dispatch-Lesecallback im Treiber hat zwei Aufgaben: Erstens führt er weitere Überprüfungen durch, die der E/A-Manager nicht leisten konnte, da er keine Vorstellung davon hatte, was die Anforderung tatsächlich bedeuten sollte. Beispielsweise kann der Treiber prüfen, ob der für eine Lese- oder Schreib­ operation bereitgestellte Puffer groß genug ist oder ob der für eine DeviceIoControl-Ope­ ration angegebene E/A-Steuercode unterstützt wird. Schlägt eine dieser Überprüfungen fehl, schließt der Treiber das IRP mit dem Status »fehlgeschlagen« ab (IoCompleteRequest) und gibt sofort die Steuerung zurück. Bei bestandenen Überprüfungen dagegen ruft der Treiber seine E/A-Startroutine auf, um die Operation auszulösen. Ist das Hardwaregerät jedoch gerade beschäftigt (weil es sich um ein vorheriges IRP kümmert), wird das IRP in eine vom Treiber verwaltete Warteschlange gestellt. In diesem Fall wird STATUS_PENDING zurückgegeben, ohne das IRP abzuschließen. Der E/A-Manager nutzt dazu die Funktion IoStartPacket, die das Busy-Bit im Geräteobjekt überprüft und das IRP in die Warteschlange stellt, wenn das Gerät beschäftigt ist. (Die Warteschlange ist dabei ein Teil der Struktur für das Geräteobjekt.) Ist das Gerät nicht beschäftigt, setzt der E/A-Manager das Busy-Bit und ruft die registrierte E/A-Startroutine auf (es gibt ein entsprechendes Element in dem Treiberobjekt, das in DriverEntry initialisiert wurde). Selbst wenn der Treiber IoStartPacket nicht verwendet, folgt er einer ähnlichen Logik. 4. Wenn das Gerät nicht beschäftigt ist, wird die E/A-Startroutine unmittelbar von der Dispatchroutine aufgerufen, was bedeutet, dass dieser Aufruf immer noch durch den anfordernden Thread erfolgt. In Abb. 6–14 dagegen wird die E/A-Startroutine im Kontext eines beliebigen Threads aufgerufen. Wie Sie bei der Betrachtung der DPC-Routine in Schritt 8 sehen werden, ist das im Allgemeinen der Fall. Die E/A-Startroutine ist dazu da, das Hardwaregerät mithilfe der relevanten Parameter für das IRP zu programmieren (indem sie beispielsweise mithilfe von HAL-Hardwarezugriffsroutinen wie WRITE_PORT_UCHAR, WRITE_REGISTER_ULONG usw. in dessen Port oder Register schreibt). Nach dem Abschluss der E/A-Startroutine springt der Aufruf zurück. Im Treiber läuft jetzt kein besonderer Code, die Hardware funktioniert und erledigt ihre Aufgabe. Während das Hardwaregerät arbeitet, können weitere Anweisungen für das Gerät hereinkommen, und zwar sowohl über denselben Thread (bei Verwendung asynchroner Operationen) als auch über andere Threads, die Handles für das Gerät geöffnet haben. Wenn das passiert, erkennt die Dispatchroutine, dass das Gerät beschäftigt ist, und stellt das IRP in die Warteschlange. (Wie bereits erwähnt, besteht eine Möglichkeit dazu, IoStartPacket aufzurufen.) 5. Wenn das Gerät die laufende Operation abgeschlossen hat, löst es einen Interrupt aus. Der Traphandler des Kernels speichert den CPU-Kontext für den Thread, der gerade auf der zur Handhabung des Interrupts ausgewählten CPU läuft, hebt die IRQL dieser CPU auf die für den Interrupt an (DIRQL) und springt zu der registrierten ISR für das Gerät. 6. Die ISR, die auf einer Geräte-IRQL läuft (größer 2), führt so wenig Arbeiten durch wie möglich, weist das Gerät an, das Interruptsignal zu beenden, und ruft den Status oder andere erforderliche Informationen von dem Hardwaregerät ab. Als letzte Maßnahme stellt die ISR einen DPC für die weitere Verarbeitung auf einer niedrigeren IRQL in eine Warteschlange. Der Vorteil der Verwendung eines DPCs für den Großteil der Geräteaufgaben E/A-Verarbeitung Kapitel 6 605
besteht darin, dass blockierte Interrupts mit IRQLs zwischen der Geräte-IRQL und der DPC/ Dispatch-IRQL (2) auftreten können, bevor der DPC mit niedrigerer Priorität verarbeitet wird. Interrupts auf einer mittleren Ebene werden daher schneller verarbeitet, als es sonst möglich wäre, was die Latenz des Systems verringert. 7. Nachdem der Interrupt verworfen wurde, erkennt der Kernel, dass die DPC-Warteschlange nicht leer ist, und verwendet daher einen Softwareinterrupt auf der IRQL DPC_LEVEL (2), um zu der DPC-Verarbeitungsschleife zu springen. 8. Schließlich wird der DPC aus der Warteschlange genommen und auf IRQL 2 ausgeführt, wobei er gewöhnlich die beiden folgenden Hauptoperationen erledigt: 9. • Er ruft ggf. das nächste IRP in der Warteschlange auf und beginnt mit der neuen Operation für das Gerät. Dies geschieht als Erstes, damit das Gerät nicht zu lange im Leerlauf ist. Hat die Dispatchroutine IoStartPacket verwendet, dann hat die DPC-Routine deren Gegenstück IoStartNextPacket aufgerufen, die die hier beschriebene Aufgabe ausführt. Ist ein IRP verfügbar, wird die E/A-Startroutine vom DPC aufgerufen. Aus diesem Grunde erfolgt dieser Aufruf im Allgemeinen im Kontext eines willkürlichen Threads. Befinden sich keine IRPs in der Warteschlange, wird das Gerät als unbeschäftigt gekennzeichnet, also als bereit für die nächste eingehende Anforderung. • Er schließt das IRP ab, dessen Operation gerade von dem Treiber durch Aufruf von IoCompleteRequest beendet wurde. Jetzt ist der Treiber nicht mehr für das IRP verantwortlich. Es sollte auch nicht mehr angefasst werden, da es nach dem Aufruf jeden Augenblick freigegeben werden kann. IoCompleteRequest ruft jegliche registrierten Beendigungsroutinen ab. Schließlich gibt der E/A-Manager das IRP frei (wozu er eine eigene Beendigungsroutine verwendet). Der ursprünglich anfordernde Thread muss über den Abschluss benachrichtigt werden. Der DPC wird aber zurzeit in einem beliebigen Thread ausgeführt, also sehr wahrscheinlich nicht in dem ursprünglichen Thread mit dem ursprünglichen Prozessadressraum. Um Code im Kontext des anfordernden Threads aufzuführen, wird ein besonderer Kernel-APC an den Thread ausgegeben. Ein APC ist eine Funktion, die zwangsweise im Kontext eines bestimmten Threads ausgeführt wird. Wenn der anfordernde Thread CPU-Zeit erhält, wird als Erstes der besondere Kernel-APC ausgeführt (auf der IRQL APC_LEVEL, also 1). Er erledigt die erforderlichen Aufgaben, also z. B. die Befreiung des Threads aus dem Wartezustand, die Signalisierung eines in einer asynchronen Operation registrierten Ereignisses usw. (Mehr über APCs erfahren Sie in Kapitel 8 von Band 2.) Noch ein letztes Wort zur E/A-Beendigung: Die asynchronen E/A-Funktionen ReadFileEx und WriteFileEx erlauben dem Aufrufer, eine Callbackfunktion als Parameter bereitzustellen. Wenn der Aufrufer diese Vorkehrung nutzt, stellt der E/A-Manager im letzten Schritt der E/A-Beendigung einen Benutzermodus-APC in die APC-Warteschlange im Thread des Aufrufers. Dadurch kann der Aufrufer eine Subroutine angeben, die aufgerufen wird, wenn eine E/A-Anforderung abgeschlossen oder abgebrochen wird. APC-Beendigungsroutinen im Benutzermodus werden im Kontext des anfordernden Threads ausgeführt und nur dann ausgeliefert, wenn der Thread in einen benachrichtigungsfähigen Wartezustand eintritt (durch den Aufruf von Funktionen wie SleepEx, WaitForSingleObjectEx und WaitForMultipleObjectsEx). 606 Kapitel 6 Das E/A-System
Zugriff auf den Benutzer-Adressraumpuffer Wie Abb. 6–14 zeigt, sind an der Verarbeitung eines IRPs vier Haupttreiberfunktionen beteiligt. Diese Routinen müssen unter Umständen auf den von der Clientanwendung bereitgestellten Puffer im Benutzerraum zugreifen. Wenn eine Anwendung oder ein Gerätetreiber mithilfe der Systemdienste NtReadFile, NtWriteFile oder NtDeviceIoControlFile (oder den entsprechenden Windows-API-Funktionen ReadFile, WriteFile und DeviceIoFile) indirekt ein IRP erstellt, wird der Zeiger auf den Puffer des Benutzers im Element UserBuffer des IRP-Rumpfes bereitgestellt. Ein direkter Zugriff auf diesen Puffer kann jedoch nur im Kontext des anfordernden Threads (in dem der Prozessadressraum des Clients sichtbar ist) und auf IRQL 0 erfolgen (auf der die Auslagerung normal verläuft). Wir bereits im vorherigen Abschnitt erwähnt, erfüllt nur die Dispatchroutine diese beiden Kriterien – und das nicht einmal in jedem Fall! Es ist möglich, dass ein oberer Filter das IRP festhält und nicht sofort nach unten übergibt, sondern später über einen anderen Thread, was obendrein auf der CPU-IRQL 2 oder höher geschehen kann. Die drei anderen Funktionen (E/A-Start, ISR und DPC) laufen auf jeden Fall im Kontext eines beliebigen Threads und auf IRQL 2 (DIRQL für die ISR). Ein direkter Zugriff auf den Benutzerpuffer von einer dieser Routinen aus ist gewöhnlich katastrophal, und zwar aus folgenden Gründen: QQ Da der Thread auf IRQL 2 oder höher ausgeführt wird, ist keine Auslagerung erlaubt. Der Benutzerpuffer (oder Teile davon) können aber ausgelagert sein, weshalb ein Zugriff auf den nicht residenten Speicher zu einem Absturz führt. QQ Da die Funktionen in einem willkürlichen Thread ausgeführt werden und daher ein zufälliger Prozessadressraum sichtbar ist, hat die ursprüngliche Benutzerraumadresse keine Bedeutung. Ein Zugriff darauf führt sehr wahrscheinlich zu einer Zugriffsverletzung oder, was noch schlimmer ist, zu einem Zugriff auf Daten eines willkürlichen Prozesses (des Elternprozesses des zurzeit ausgeführten Threads). Es muss daher eine gefahrlose Vorgehensweise geben, um von diesen Routinen aus auf den Benutzerpuffer zuzugreifen. Der E/A-Manager bietet dafür zwei Möglichkeiten, für die er die Hauptarbeit übernimmt, nämlich gepufferte und direkte E/A. Es gibt noch eine dritte Möglichkeit, die »Weder/noch-E/A« (Neither I/O), die aber keine Vorgehensweise im Sinne des Wortes ist. Dabei tut der E/A-Manager nichts, sondern lässt das Problem vom Treiber lösen. Der Treiber wählt die einzusetzende Methode wie folgt aus: QQ Für Lese- und Schreibanforderungen (IRP_MJ_READ und IRP_MJ_WRITE) setzt er das Element Flags (in einer booleschen Oder-Operation, um andere Flags nicht zu beeinträchtigen) des Geräteobjekts (DEVICE_OBJECT) auf DO_BUFFERED_IO (für gepufferte E/A) oder DO_DIRECT_IO (für direkte E/A). Ist keines dieser Flags gesetzt, so bedeutet das Neither I/O. (DO steht hier für Device Object, also Geräteobjekt.) QQ Für Gerätesteuerungsanforderungen (IRP_MJ_DEVICE_CONTROL) werden die einzelnen Steuercodes mithilfe des Makros CTL_CODE zusammengesetzt. Einige der Bits dienen dabei zur Angabe der Puffermethode. Dadurch kann die Puffermethode für jeden Steuercode einzeln festgelegt werden, was sehr praktisch ist. E/A-Verarbeitung Kapitel 6 607
Im folgenden Abschnitt werden die Puffermethoden ausführlich beschrieben. Gepufferte E/A Bei der gepufferten E/A weist der E/A-Manager im nicht ausgelagerten Pool einen Spiegelpuffer zu, der die gleiche Größe hat wie der Benutzerpuffer, und speichert im Element AssociatedIrp.SystemBuffer des IRP-Rumpfes einen Zeiger auf diesen neuen Puffer. Abb. 6–15 zeigt die Hauptphasen der gepufferten E/A für eine Leseoperation, wobei der Vorgang bei Schreiboperationen ähnlich ist. Kernelraum Kernelraum Kernelpuffer RAM Benutzerpuffer Benutzerpuffer Benutzerraum Benutzerraum Der Benutzerclient weist den Puffer zu und ruft die Leseoperation auf Der E/A-Manager weist den Puffer im nicht ausgelagerten Pool zu Kernelraum Kernelraum Daten Daten Kopieren RAM Benutzerpuffer Benutzerraum Der Treiber schreibt Daten in den Kernelpuffer Daten Daten Benutzerraum Nach dem Abschluss des IRPs kopiert der E/A-Manager die Daten in den Benutzer­ puffer und gibt den Kernel­puffer frei Abbildung 6–15 Gepufferte E/A Der Treiber kann von jedem Thread aus und auf jeder IRQL auf den Systempuffer zugreifen (Adresse q in Abb. 6–15): QQ Die Adresse befindet sich im Systemraum und ist daher in jedem Prozesskontext gültig. QQ Der Puffer wird im nicht ausgelagerten Pool zugewiesen, weshalb keine Seitenfehler auftreten können. Bei Schreiboperationen kopiert der E/A-Manager die Pufferdaten des Aufrufers in den zugewiesenen Puffer, wenn er das IRP erstellt. Bei Leseoperationen dagegen kopiert er die Daten aus dem zugewiesenen Puffer in den Benutzerpuffer, wenn das IRP (mithilfe eines besonderen Kernel-APCs) abgeschlossen wird, und gibt dann den zugewiesenen Puffer frei. 608 Kapitel 6 Das E/A-System
Die gepufferte E/A lässt sich sehr einfach einsetzen, da sich der E/A-Manager praktisch um alles kümmert. Ihr größter Nachteil besteht darin, dass sie immer einen Kopiervorgang erfordert, was bei umfangreichen Puffern ineffizient ist. Gewöhnlich wird die gepufferte E/A nur eingesetzt, wenn der Puffer nicht größer als eine Seite ist (4 KB) und das Gerät keinen direkten Speicherzugriff (Direct Memory Access, DMA) unterstützt. DMA dient zur Übertragung von Daten von einem Gerät in den RAM und umgekehrt ohne Umweg über die CPU, doch da die gepufferte E/A immer einen Kopiervorgang mithilfe der CPU einschließt, würde sie DMA sinnlos machen. Direkte E/A Direkte E/A bietet einem Treiber die Möglichkeit, unmittelbar auf den Benutzerpuffer zuzugreifen, ohne dass ein Kopiervorgang erforderlich ist. Abb. 6–16 zeigt die Hauptphasen der direkten E/A für Lese- und Schreiboperationen. Kernelraum Kernelraum Benutzerpuffer Benutzerpuffer Benutzerraum Benutzerraum Der Benutzerclient weist den Puffer zu und ruft die Leseoder Schreib­operation auf Der E/A-Manager verriegelt den Benutzerpuffer im RAM und bildet den Puffer im Systemraum ab Kernelraum Daten Daten Daten Benutzerraum Der Treiber liest/schreibt unter Verwendung der Systemadresse im Puffer Abbildung 6–16 Direkte E/A Wenn der E/A-Manager das IRP erstellt, ruft er die (im WDK dokumentierte) Funktion ­MmProbeAndLockPages auf, um den Benutzerpuffer im Speicher zu verriegeln (sodass dieser nicht ausgelagert werden kann). Außerdem speichert der E/A-Manager eine Speicherdeskriptorliste (Memory Descriptor List, MDL). Dabei handelt es sich um eine Struktur, die den von einem Puffer belegten physischen Speicher beschreibt. Ihre Adresse wird im Element MdlAddress des IRP-Rumpfes festgehalten. Geräte, die DMA durchführen, benötigen nur physische Beschreibungen der Puffer, weshalb eine MDL für ihren Betrieb ausreicht. Muss ein Treiber dagegen auf E/A-Verarbeitung Kapitel 6 609
den Pufferinhalt zugreifen, so kann er den Puffer im Systemadressraum abbilden. Dazu verwendet er die Funktion MmGetSystemAddressForMdlSafe, der er die bereitgestellte MDL übergibt. Der resultierende Zeiger (q in Abb. 6–16) kann gefahrlos in jedem Threadkontext (es handelt sich um eine Systemadresse) und auf jeder IRQL verwendet werden (der Puffer kann nicht ausgelagert werden). Der Benutzerpuffer wird also letzten Endes doppelt zugewiesen. Die direkte Benutzeradresse (p in Abb. 6–16) kann nur im Kontext des ursprünglichen Prozesses verwendet werden, die zweite Zuordnung im Systemraum dagegen in jedem Kontext. Nach Abschluss des IRPs entriegelt der E/A-Manager den Puffer (sodass dieser wieder ausgelagert werden kann), indem er MmUnlockPages aufruft (im WDK dokumentiert). Die direkte E/A ist für große Puffer (von mehr als einer Seite) sinnvoll, da hierbei kein Kopiervorgang stattfindet, vor allem aber auch für DMA-Übertragungen (aus demselben Grund). Weder/noch Bei Neither I/O führt der E/A-Manager keinerlei Pufferverwaltung durch, sondern überlässt diese Aufgabe dem Gerätetreiber. Dabei ist es möglich, dass der Treiber die Schritte vornimmt, die der E/A-Manager bei den anderen beiden Formen der Pufferverwaltung ausführt. In manchen Fällen reicht es jedoch auch, in der Dispatchroutine auf den Puffer zuzugreifen, sodass der Treiber nichts mehr tun muss. Der Hauptvorteil von Neither I/O ist der fehlende Zusatzaufwand. Treiber, die Neither I/O für den Zugriff auf möglicherweise im Benutzerraum befindliche Puffer verwenden, müssen darauf achten, dass die Pufferadressen gültig sind und sich nicht auf Kernelspeicher beziehen. Skalarwerte können jedoch gefahrlos übergeben werden, wobei jedoch nur bei sehr wenigen Treibern eine Notwendigkeit dazu besteht. Eine mangelnde Überprüfung der Adressen kann zu Abstürzen und Sicherheitsproblemen führen, etwa dadurch, dass Anwendungen Zugriff auf Kernelmodusspeicher bekommen oder Code in den Kernel einschleusen können. Die Funktionen ProbeForRead und ProbeForWrite, die der Kernel für Treiber bereitstellt, stellen sicher, dass sich der Puffer komplett im Benutzeradressraum befindet. Um einen Absturz aufgrund eines Verweises auf eine ungültige Benutzermodusadresse zu vermeiden, können Treiber mit strukturierter Ausnahmebehandlung (Structured Exception Handling, SEH) auf Benutzermoduspuffer zugreifen. Diese Vorgehensweise wird in C/C++ durch __try/­ __except-Blöcke dargestellt und dient dazu, Fehler aufgrund eines ungültigen Speicherzugriffs abzufangen und in Fehlercodes zu übersetzen, die an die Anwendung zurückgegeben werden. (Mehr über SEH erfahren Sie in Kapitel 8 von Band 2.) Außerdem können Treiber alle Eingabedaten abfangen und in einen Kernelpuffer stellen, anstatt sich auf Benutzermodusadressen zu verlassen, da der Aufrufer auch dann die Daten hinter dem Rücken des Treibers ändern kann, wenn die Speicheradresse selbst gültig ist. Synchronisierung Treiber müssen ihren Zugriff auf globale Treiberdaten und Hardwareregister aus den beiden folgenden Gründen synchronisieren: QQ Die Ausführung eines Treibers kann durch Threads höherer Priorität oder den Ablauf einer Zeitscheibe (oder eines Quantums) vorzeitig abgebrochen oder durch Interrupts einer höheren IRQL unterbrochen werden. QQ Auf Multiprozessorsystemen (die heutzutage die Norm sind) kann Windows Treibercode gleichzeitig auf mehreren Prozessoren ausführen. 610 Kapitel 6 Das E/A-System
Ohne Synchronisierung kann eine Datenbeschädigung auftreten. Beispielsweise kann Gerätetreibercode, der auf der passiven IRQL 0 läuft (z. B. eine Dispatchroutine), wenn ein Aufrufer eine E/A-Operation auslöst, durch einen Geräteinterrupt unterbrochen werden. Dadurch wird die ISR des Gerätetreibers ausgeführt, während der Gerätetreiber selbst schon läuft. War der Gerätetreiber dabei, Daten zu ändern, die auch von der ISR geändert werden – z. B. Geräteregister, Heapspeicher oder statische Daten –, dann können die Daten bei der Ausführung der ISR beschädigt werden. Um eine solche Situation zu verhindern, müssen für Windows geschriebene Gerätetreiber ihren Zugriff auf jegliche Daten synchronisieren, die auf mehr als einer IRQL zugänglich sind. Bevor der Gerätetreiber versucht, die gemeinsamen Daten zu ändern, muss er alle anderen Threads (bzw. CPUs auf Mehrprozessorsystemen) sperren, damit sie nicht dieselbe Datenstruktur bearbeiten. Auf einem Einzelprozessorsystem ist die Synchronisierung von zwei oder mehr Funktionen auf verschiedenen IRQLs ganz einfach. Eine solche Funktion muss lediglich ihre IRQL auf den höchsten Wert ändern, der für sie möglich ist (mithilfe von KeRaiseIrql). Um beispielsweise eine Dispatchroutine (IRQL 0) und eine DPC-Routine (IRQL 2) zu synchronisieren, muss die Dispatchroutine ihre IRQL auf 2 anheben, bevor sie auf die gemeinsamen Daten zugreift. Ist eine Synchronisierung zwischen einem DPC und einer ISR erforderlich, hebt der DPC seine IRQL auf die Geräte-IRQL an. (Diese Information wird dem Treiber bereitgestellt, wenn der PnP-Manager ihn über die Hardwareressourcen informiert, mit denen das Gerät verbunden ist.) Auf Mehrprozessorsystemen dagegen reicht eine Anhebung der IRQL nicht aus, da die andere Routine – etwa eine ISR – auch auf einer anderen CPU ausgeführt werden kann. (Denken Sie daran, dass die IRQL ein Attribut der CPU und keine globale Systemeigenschaft ist.) Um eine CPU-übergreifende Synchronisierung zu ermöglichen, stellt der Kernel mit den Spinlocks besondere Synchronisierungsobjekte bereit. Im Folgenden sehen wir uns Spinlocks nur kurz im Zusammenhang mit der Treibersynchronisierung an. Eine ausführliche Darstellung erhalten Sie in Kapitel 8 von Band 2. Im Prinzip ähnelt ein Spinlock einem Mutex (ebenfalls in Kapitel 8 von Band 2 erklärt), da beide dem Code erlauben, auf gemeinsam genutzte Daten zuzugreifen. Allerdings gibt es deutliche Unterschiede in der Funktionsweise und Anwendung. Tabelle 6–3 gibt einen Überblick über die Unterschiede zwischen Mutexen und Spinlocks. Mutex Spinlock Art der Synchronisierung Ein Thread von vielen erhält die Erlaubnis, in eine kritische Region einzusteigen und auf gemeinsame Daten zuzugreifen. Eine CPU von vielen erhält die Erlaubnis, in eine kritische Region einzusteigen und auf gemeinsame Daten zuzugreifen. Mögliche IRQL < DISPATCH_LEVEL (2) >= DISPATCH_LEVEL (2) Wartetyp Normal, das heißt, es werden beim Warten keine CPU-Zyklen verbraucht. Beschäftigt, das heißt, die CPU prüft fortlaufend das Spinlockbit, bis es frei ist. Besitz Der Besitzerthread wird festgehalten. Rekursiver Erwerb der Sperre ist zulässig. Der CPU-Besitzer wird nicht festgehalten. Rekursiver Erwerb der Sperre führt zu einem Deadlock. Tabelle 6–3 Mutexe und Spinlocks im Vergleich E/A-Verarbeitung Kapitel 6 611
Bei einem Spinlock handelt es sich einfach nur um ein Bit im Arbeitsspeicher, auf das eine elementare Test- und Änderungsoperation zugreift. Spinlocks können im Besitz einer CPU oder frei (ohne Besitzer) sein. Wie Sie in Tabelle 6–3 sehen, sind Spinlocks für die Synchronisierung auf höheren IRQLs (>= 2) erforderlich. Ein Mutex lässt sich hier nicht verwenden, da dazu ein Scheduler nötig ist, der auf einer CPU mit einer IRQL von 2 oder höher nicht aufwachen kann. Aus diesem Grund ist die CPU auch beim Warten auf ein Spinlock beschäftigt: Der Thread kann nicht in einen normalen Wartezustand übergeben, da dazu ein Scheduler aufwachen und zu einem anderen Thread umschalten müsste. Der Erwerb eines Spinlocks durch eine CPU läuft immer in zwei Schritten ab. Als Erstes wird die IRQL auf diejenige erhöht, auf der die Synchronisierung stattfinden soll, also auf die höchste IRQL, auf der die zu synchronisierende Funktion ausgeführt werden kann. Beispielsweise muss die IRQL bei der Synchronisierung einer Dispatchroutine (IRQL 0) und eines DPCs (2) auf 2 erhöht werden und bei der Synchronisierung eines DPCs (2) und einer ISR (DIRQL) auf die IRQL (die DIRQL für den vorliegenden Interrupt). Als Zweites wird der Erwerb des Spinlocks versucht, indem das Spinlockbit überprüft und gesetzt wird. Die hier aufgezeigten Schritte, um ein Spinlock zu erwerben, sind vereinfacht. Einzelheiten, die für unser aktuelles Thema nicht von Belang sind, finden hier keine Beachtung. Eine vollständige Beschreibung finden Sie in Kapitel 8 von Band 2. Funktionen, die Spinlocks erwerben, bestimmen die IRQL für die Synchronisierung, wie Sie in Kürze sehen werden. Abb. 6–17 zeigt eine vereinfachte Darstellung des zweiteiligen Vorgangs zum Erwerb eines Spinlocks. Anheben auf die zugehörige IRQL Testen und Setzen des Spinlockbits Nein War das Bit zuvor frei? Ja This CPU now owns the Spinklock Abbildung 6–17 Erwerb eines Spinlocks Für die Synchronisierung auf IRQL 2 – etwa zwischen einer Dispatchroutine und einem DPC oder zwischen zwei DPCs auf unterschiedlichen CPUs – stellt der Kernel die Funktionen K­ eAcquireSpinLock und KeReleaseSpinLock bereit. (Es gibt noch andere Varianten, die in Ka- 612 Kapitel 6 Das E/A-System
pitel 8 von Band 2 vorgestellt werden.) Diese Funktionen führen die Schritte aus Abb. 6–17 durch, wobei die »zugehörige IRQL« 2 ist. In diesem Fall muss der Treiber ein Spinlock zuweisen (KSPIN_LOCK; lediglich 4 Bytes auf 32-Bit- und 8 Bytes auf 64-Bit-Systemen) – gewöhnlich in der Geräteerweiterung (wo die vom Treiber verwalteten Daten für das Gerät aufbewahrt werden) – und es mit KeInitializeSpinLock initialisieren. Zur Synchronisierung von beliebigen Funktionen (etwa einem DPC oder einer Dispatchroutine) mit der ISR müssen andere Funktionen verwendet werden. Jedes Interruptobjekt (­KINTERRUPT) enthält ein Spinlock, das vor der Ausführung der ISR erworben wird. (Das bringt es mit sich, dass ein und dieselbe ISR nicht gleichzeitig auf mehreren CPUs ausgeführt werden kann.) Die Synchronisierung erfolgt in diesem Fall mit diesem Spinlock (es muss kein weiteres mehr zugewiesen werden), das indirekt durch die Funktion KeAcquireInterruptSpinLock erworben und mit KeReleaseInterruptSpinLock freigegeben werden kann. Eine weitere Möglichkeit bietet die Funktion KeSynchronizeExecution, die eine vom Treiber bereitgestellte Callbackfunktion entgegennimmt. Letztere wird zwischen dem Erwerb und der Freigabe des Interrupt-Spinlocks aufgerufen. Es sollte nun klar geworden sein, dass ISRs zwar besondere Aufmerksamkeit erfordern, aber jegliche von einem Gerätetreiber verwendeten Daten auch für Funktionen desselben Gerätetreibers auf einem anderen Prozessor zugänglich sind. Daher ist es unverzichtbar, dass Gerätetreibercode seine Verwendung von globalen oder gemeinsamen Daten und jeglichen Zugriff auf das physische Gerät synchronisiert. E/A-Anforderungen an geschichtete Treiber Im Abschnitt »IRP-Fluss« wurden die allgemeinen Möglichkeiten aufgezeigt, die Treiber für den Umgang mit IRPs haben, wobei der Schwerpunkt auf einem standardmäßigen WDM-Geräteknoten lag. Der vorstehende Abschnitt hat gezeigt, wie eine E/A-Anforderung an ein einfaches, von einem einzigen Treiber gesteuertes Gerät gehandhabt wird. Die E/A-Verarbeitung für da­ teigestützte Geräte und für Anforderungen an andere geschichtete Treiber erfolgt sehr ähnlich. Es lohnt sich aber trotzdem, einen genaueren Blick auf die Anforderungen an Dateisystemtreiber zu werfen. Abb. 6–18 zeigt ein vereinfachtes Beispiel dafür, wie eine asynchrone E/A-Anforderung die geschichteten Treiber für Nicht-Hardwaregeräte als Hauptziel durchlaufen kann. Als Beispiel dient hier eine Festplatte, die von einem Dateisystem gesteuert wird. E/A-Verarbeitung Kapitel 6 613
Clientprozess Gibt Status »E/A ausstehend« Benutzermodus zurück Ruft E/A-Dienst auf Kernelmodus Der E/A-Manager erstellt das IRP, füllt die erste IRPStackposition aus und ruft einen Dateisystemtreiber auf E/A-Manager Gibt Status »E/A ausstehend« zurück Aktuell Der Dateisystemtreiber füllt die zweite IRP-Stackposition aus und ruft den VolumeManager auf Dateisystem­ treiber Gibt Status »E/A ausstehend« zurück VolumeManager Aktuell Sendet das IRP nach unten zum Festplattentreiber (oder stellt es in eine Warteschlange) und springt zurück Abbildung 6–18 Verlauf einer asynchronen Anforderung durch geschichtete Treiber Auch hier empfängt der E/A-Manager wieder die Anforderung und erstellt ein IRP, um sie darzustellen. Dieses Mal jedoch liefert er das Paket an einen Dateisystemtreiber, der zu diesem Zeitpunkt großen Einfluss auf die E/A-Operation hat. Je nach Art der Anforderung kann das Dateisystem das ursprüngliche IRP gleich an den Festplattentreiber senden oder zusätzliche IRPs erstellen, die es dann getrennt an den Festplattentreiber schickt. Am wahrscheinlichsten ist es, dass das Dateisystem das ursprüngliche IRP verwendet, wenn die Anforderung einer einzelnen, einfachen Anforderung an ein Gerät entspricht. Wenn beispielsweise eine Anwendung eine Leseanforderung für die ersten 512 Bytes in einer Datei auf einem Volume ausgibt, ruft das NTFS-Dateisystem einfach den Volume-Manager-Treiber auf und weist ihn an, einen Sektor des Volumes beginnend an der Anfangsposition der Datei zu lesen. Nachdem der DMA-Adapter des Festplattencontrollers die Datenübertragung abgeschlossen hat, unterbricht der Controller den Host. Dadurch wird die ISR für den Festplattencontroller ausgeführt und fordert einen DPC-Callback an, um das IRP abzuschließen (siehe Abb. 6–19). Anstatt das ursprüngliche IRP zu verwenden, kann das Dateisystem auch mehrere zugehörige IRPs erstellen, die parallel für eine einzige E/A-Anforderung ausgeführt werden. Sind die von einer Datei zu lesenden Daten beispielsweise über die Festplatte verstreut, kann der Dateisystemtreiber mehrere IRPs anlegen, die jeweils einen Teil der Anforderung in einem einzelnen Sektor erfüllen. Dies wird in Abb. 6–20 dargestellt. 614 Kapitel 6 Das E/A-System
Clientprozess Beim Abschluss der E/A werden die Ergebnisse im Adressraum des Aufrufers zurückgegeben Benutzermodus Kernelmodus E/A-Manager Der Dateisystemtreiber führt jegliche erforderlichen Aufräumarbeiten aus Aktuell Dateisystem­ treiber Der Volume-Manager führt jegliche erforderlichen Aufräumarbeiten aus VolumeManager Festplatten­ treiber Aktuell Ein Interrupt auf Geräteebene tritt auf. Der Festplattentreiber handhabt den Interrupt und stellt einen DPC zur Vervollständigung der E/A in die Warteschlange. Dieser DPC springt zurück zur aktuellen IRP-Stackposition und ruft den Volume-Manager auf. Abbildung 6–19 Abschluss einer geschichteten E/A-Anforderung Clientprozess Gibt Status »E/A ausstehend« Benutzermodus zurück Ruft E/A-Dienst auf Kernelmodus Der E/A-Manager erstellt das IRP, füllt die erste IRPStackposition aus und ruft einen Dateisystemtreiber auf E/A-Manager Gibt Status »E/A ausstehend« zurück Dateisystem­ treiber Der Dateisystemtreiber erstellt zugehörige IRPs auf und ruft den Volume-Manager einmal oder mehrere Male auf Gibt Status »E/A ausstehend« zurück VolumeManager Sendet die IRPs zum Festplatten­ treiber und springt zurück Abbildung 6–20 Verarbeitung mithilfe zugehöriger IRPs E/A-Verarbeitung Kapitel 6 615
Der Dateisystemtreiber liefert die zugehörigen IRPs an den Volume-Manager, der sie wiederum an den Festplattengerätetreiber schickt. Dieser stellt sie in die Warteschlange für das Festplattengerät. Alle IRPs werden gleichzeitig verarbeitet, wobei der Dateisystemtreiber die zurückgegebenen Daten im Auge behält. Sind alle zugehörigen IRPs abgeschlossen, schließt das E/A-System das ursprüngliche IRP ab und kehrt zum Aufrufer zurück (siehe Abb. 6–21). Clientprozess Benutzermodus Nach Beendigung aller zugehörigen IRPs wird das ursprüngliche IRP abgeschlossen, und Status und Daten werden an den Aufrufer zurückgegeben Kernelmodus E/A-Manager Schritt 2 wird wiederholt. Dabei werden die IRPs 2 bis n abgeschlossen. Der Dateisystemtreiber führt jegliche erforderlichen Aufräumarbeiten aus Dateisystem­ treiber Schritt 1 wird wiederholt. Dabei werden die IRPs 2 bis n abgeschlossen. Der Volume-Manager führt jegliche erforderlichen Aufräumarbeiten aus VolumeManager Festplatten­ treiber Nach Übertragung der Daten für ein IRP tritt ein Geräteinterrupt auf. Der Festplattentreiber handhabt den Interrupt und stellt einen DPC in die Warteschlange. Dieser DPC startet das nächste IRP für das Gerät und ruft den E/A-Manager auf, um das erste IRP abzuschließen. Abbildung 6–21 Abschluss einer E/A-Anforderung mit zugehörigen IRPs Alle Windows-Dateisystemtreiber zur Verwaltung von Festplatten-Dateisystemen gehören zu einem Treiberstack mit mindestens drei Ebenen – dem Dateisystemtreiber an der Spitze, einem Volume-Manager in der Mitte und einem Festplattentreiber unten. Zusätzlich können beliebig viele Filtertreiber zwischen diese Treiber geschaltet sein. Der Übersichtlichkeit halber wurden in dem vorstehenden Beispiel für geschichtete E/A-Anforderungen lediglich der Dateisystemtreiber und der Volume-Manager dargestellt. Weitere Informationen erhalten Sie in Kapitel 12 von Band 2. Threadagnostische E/A In den bis jetzt beschriebenen E/A-Modellen werden die IRPs in die Warteschlange des Threads gestellt, der die E/A ausgelöst hat, und von dem E/A-Manager durch die Ausgabe eines APCs an diesen Thread abgeschlossen, sodass der prozess- und threadspezifische Kontext bei der Verarbeitung dieses Abschlusses zugänglich sind. Für die Leistungs- und Skalierbarkeitsbedürfnisse 616 Kapitel 6 Das E/A-System
der meisten Anwendungen reicht eine solche threadspezifische E/A-Verarbeitung gewöhnlich aus. Mithilfe der beiden folgenden Mechanismen bietet Windows jedoch auch Unterstützung für threadagnostische E/A: QQ E/A-Vervollständigungsports. Sie werden im gleichnamigen Abschnitt weiter hinten in diesem Kapitel beschrieben. QQ Verriegeln des Benutzerpuffers im Arbeitsspeicher und Abbilden des Puffers in den System­ adressraum Bei der Verwendung von E/A-Vervollständigungsports entscheidet die Anwendung, wann sie auf Abschluss der E/A prüft. Daher ist es nicht von Bedeutung, welcher Thread die E/A-­Anforderung ausgegeben hat, da auch jeder andere Thread die Vervollständigungsanforderung durchführen kann. Das IRP kann daher im Kontext eines beliebigen Threads abgeschlossen werden, der Zugriff auf den Vervollständigungsport hat. Auch bei der Verwendung einer verriegelten und im Kernel abgebildeten Version des Benutzerpuffers ist es nicht erforderlich, sich im selben Speicheradressraum zu befinden wie der ausgebende Thread, da der Kernel auf Speicher beliebiger Kontexte zugreifen kann. Anwendungen können diesen Mechanismus einschalten, indem sie SetFileIoOverlappedRange verwenden, sofern sie über das Recht SeLockMemoryPrivilege verfügen. Sind sowohl die Vervollständigsport-E/A als auch die E/A auf Dateipuffern durch SetFile­ IoOverlappedRange festgelegt, verknüpft der E/A-Manager die IRPs mit dem Dateiobjekt, für das sie ausgegeben wurden, statt mit dem ausgebenden Thread. Die Erweiterung !fileobj in WinDbg zeigt eine Liste von IRPs für Dateiobjekte, für die dieser Mechanismus verwendet wird. In den nächsten Abschnitten erfahren Sie, wie die threadagnostische E/A die Zuverlässigkeit und Leistung von Anwendungen in Windows verbessert. Abbrechen der E/A Es gibt viele Möglichkeiten zur IRP-Verarbeitung und verschiedene Möglichkeit, um eine E/A-Anforderung abzuschließen. Sehr viele E/A-Operationen enden jedoch nicht mit einem Abschluss, sondern mit einem Abbruch. Beispielsweise kann es sein, dass ein Gerät entfernt werden muss, während noch IRPs aktiv sind, oder dass der Benutzer eine langwierige Operation an einem Gerät abbricht, z. B. eine Netzwerkoperation. Auch die Beendigung eines Threads oder Prozesses erfordert ein Abbrechen der E/A, da die damit verbundenen E/A-Vorgänge nicht mehr von Bedeutung sind und der Thread erst gelöscht werden kann, wenn alle ausstehenden E/A-Vorgänge abgeschlossen sind. Der E/A-Manager von Windows arbeitet mit den Treibern zusammen und muss auf effiziente und zuverlässige Weise mit solchen Anforderungen umgehen, damit die Vorgänge für die Benutzer reibungslos erscheinen. Für E/A-Operationen, die sich abbrechen lassen (gewöhnlich sind das solche, die sich noch in der Warteschlange befinden und noch nicht ausgeführt werden), registrieren Treiber eine Abbruchroutine, die beim E/A-Manager aufgerufen werden kann. Wenn ein Treiber seine Aufgaben in dieser Situation nicht erfüllt, können die Benutzer auf nicht beendbare Prozesse stoßen, die zwar in der grafischen Oberfläche verschwunden sind, aber immer noch im Task-Manager oder in Process Explorer angezeigt werden. E/A-Verarbeitung Kapitel 6 617
Abbrechen der E/A durch den Benutzer Die meisten Softwareprogramme verwenden einen Thread für Eingaben von der Benutzeroberfläche (UI) und einen oder mehrere Threads, um die Arbeit zu erledigen, darunter auch die E/A. Wenn ein Benutzer eine Operation abbrechen möchte, die in der Oberfläche ausgelöst wurde, kann es manchmal erforderlich sein, dass die Anwendung ausstehende E/A-Operationen abbrechen muss. Für Operationen, die schnell abgeschlossen sind, ist ein Abbruch nicht unbedingt erforderlich. Dagegen bietet Windows Möglichkeiten, um Operationen mit willkürlicher Dauer abzubrechen – etwa umfangreiche Datenübertragungen oder Netzwerkvorgänge –, und zwar sowohl synchrone als auch asynchrone. QQ Abbrechen synchroner E/A-Operationen Ein Thread kann CancelSynchronousIo aufrufen. Damit lassen sich sogar Erstellungsoperationen (Öffnen) abbrechen, wenn der Gerätetreiber dies unterstützt, was bei verschiedenen Windows-Treibern der Fall ist. Beispielsweise können die Treiber zur Verwaltung von Netzwerkdateisystemen (wie MUP, DFS und SMB) Operationen zum Öffnen von Netzwerkpfaden abbrechen. QQ Abbrechen asynchroner E/A-Operationen Ein Thread kann CancelIo aufrufen, um seine eigenen ausstehenden asynchronen E/A-Operationen abzubrechen. Mit CancelIoEx kann er auch alle innerhalb eines Prozesses für ein bestimmtes Dateihandle ausgegebenen asynchronen E/A-Operationen unabhängig vom auslösenden Thread abbrechen. Cancel­IoEx wirkt sich auch auf Operationen aus, die über die zuvor beschriebene Unterstützung für threadagnostische E/A in Windows mit E/A-Vervollständigungsport verknüpft sind, da sich das E/A-System die ausstehenden E/A-Operationen für einen solchen Port merkt. Die Abbildungen 6–22 und 6–23 zeigen den Abbruch synchroner bzw. asynchroner E/A-Opera­ tionen. (Für den Treiber sehen alle Abbruchvorgänge gleich aus.) Anwendung T2 übergibt das Handle von T1 Status an Anwendung Thread T1 wartet auf den Abschluss der E/A. Thread T2 fordert den Abbruch an CreateFile CancelSynchronousIo Springt sofort zurück Der E/A-Manager versucht, die synchrone E/A-Operation von T1 abzubrechen E/A-Manager Abbruchroutine aufgerufen Der Treiber gibt die Steuerung mit STATUS_CANCELLED zurück Treiber Abbildung 6–22 Abbrechen eines synchronen E/A-Vorgangs 618 Kapitel 6 Das E/A-System Abbruch­ routine
Anwendung Übergibt Dateihandle Status an Anwendung Ein Thread im Prozess fordert den Abbruch aller ausstehenden Datei-E/A-Operationen für ein bestimmtes Handle an Springt sofort zurück Der E/A-Manager versucht, alle ausstehenden E/A-Operationen für dieses Handle abzubrechen E/A-Manager Der Treiber gibt die Steuerung mit STATUS_CANCELLED zurück Abbruchroutine aufgerufen Abbruch­ routine Treiber Abbildung 6–23 Abbrechen eines asynchronen E/A-Vorgangs Abbrechen der E/A zur Beendigung des Threads Eine weitere Situation, in der E/A-Operationen abgebrochen werden müssen, liegt vor, wenn ein Thread beendet wird, entweder direkt oder weil sein Prozess beendet wird. Der E/A-Manager durchläuft die Liste der mit dem Thread verknüpften IRPs und bricht diejenigen ab, die abgebrochen werden können. Während CancelIoEx die Steuerung zurückgibt, ohne auf den Abbruch eines IRPs zu warten, lässt der Prozess-Manager eine Fortsetzung der Threadbeendigung erst dann zu, wenn alle E/A-Operationen abgebrochen wurden. Wenn ein Treiber es versäumt, ein IRP abzubrechen, bleiben das Prozess- und das Threadobjekt daher bis zum Herunterfahren des Systems zugewiesen. Nur IRPs, für die der Treiber eine Abbruchroutine eingerichtet hat, können abgebrochen werden. Der Prozess-Manager wartet, bis alle mit einem Thread verbundenen E/A-Operationen entweder abgebrochen oder abgeschlossen sind, und löscht den Thread erst dann. Experiment: Einen nicht beendbaren Prozess debuggen In diesem Experiment erstellen wir mit Notmyfault von Sysinternals einen nicht beendbaren Prozess. Notmyfault.exe verwendet den Treiber Myfault.sys und bringt ihn dazu, ein IRP aufrechtzuerhalten, für das keine Abbruchroutine registriert ist. (Notmyfault wird ausführlich im Abschnitt »Analyse von Absturzabbildern« von Kapitel 15 in Band 2 beschrieben.) Gehen Sie dazu wie folgt vor: → E/A-Verarbeitung Kapitel 6 619
1. Starten Sie Notmyfault.exe. 2. Rufen Sie im Dialogfeld Not My Fault die Registerkarte Hang auf und aktivieren Sie wie im folgenden Screenshot die Option Hang with IRP. Klicken Sie dann auf die Schaltfläche Hang. 3. Eine sichtbare Auswirkung gibt es zunächst nicht. Es ist auch nach wie vor möglich, auf Cancel zu klicken, um die Anwendung abzubrechen. Im Task-Manager und in Process Explorer dagegen wird der Prozess Notmyfault nach wie vor angezeigt. Versuche, ihn zu beenden, schlagen fehl, da Windows endlos auf den Abschluss des IRPs wartet, weil der Treiber Myfault keine Abbruchroutine dafür registriert hat. 4. Um ein solches Problem zu debuggen, können Sie sich mit WinDbg ansehen, was der Thread gerade tut. Öffnen Sie eine lokale Kerneldebuggersitzung und lassen Sie sich mit dem Befehl !process die Informationen über den Prozess Notmyfault.exe anzeigen. (Der im Folgenden gezeigte Prozess notmyfault64 ist die 64-Bit-Version.) lkd> !process 0 7 notmyfault64.exe PROCESS ffff8c0b88c823c0 SessionId: 1 Cid: 2b04 Peb: 4e5c9f4000 ParentCid: 0d40 DirBase: 3edfa000 ObjectTable: ffffdf08dd140900 HandleCount: <Data Not Accessible> Image: notmyfault64.exe VadRoot ffff8c0b863ed190 Vads 81 Clone 0 Private 493. Modified 8. Locked 0.... THREAD ffff8c0b85377300 Cid 2b04.2714 Teb: 0000004e5c808000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable fffff80a4c944018 SynchronizationEvent IRP List: ffff8c0b84f1d130: (0006,0118) Flags: 00060000 Mdl: 00000000 Not impersonating DeviceMap ffffdf08cf4d7d20 Owning Process ffff8c0b88c823c0 Image: notmyfault64.exe → 620 Kapitel 6 Das E/A-System
... Child-SP RetAddr : Args to Child : Call Site ffffb881'3ecf74a0 fffff802'cfc38a1c : 00000000'00000100 00000000'00000000 00000000'00000000 00000000'00000000 : nt!KiSwapContext+0x76 ffffb881'3ecf75e0 fffff802'cfc384bf : 00000000'00000000 00000000'00000000 00000000'00000000 00000000'00000000 : nt!KiSwapThread+0x17c ffffb881'3ecf7690 fffff802'cfc3a287 : 00000000'00000000 00000000'00000000 00000000'00000000 00000000'00000000 : nt!KiCommitThreadWait+0x14f ffffb881'3ecf7730 fffff80a'4c941fce : fffff80a'4c944018 fffff802'00000006 00000000'00000000 00000000'00000000 : nt!KeWaitForSingleObject+0x377 ffffb881'3ecf77e0 fffff802'd0067430 : ffff8c0b'88d2b550 00000000'00000001 00000000'00000001 00000000'00000000 : myfault+0x1fce ffffb881'3ecf7820 fffff802'd0066314 : ffff8c0b'00000000 ffff8c0b'88d2b504 00000000'00000000 ffffb881'3ecf7b80 : nt!IopSynchronousServiceTail+0x1a0 ffffb881'3ecf78e0 fffff802'd0065c96 : 00000000'00000000 00000000'00000000 00000000'00000000 00000000'00000000 : nt!IopXxxControlFile+0x674 ffffb881'3ecf7a20 fffff802'cfd57f93 : ffff8c0b'85377300 fffff802'cfcb9640 00000000'00000000 fffff802'd005b32f : nt!NtDeviceIoControlFile+0x56 ffffb881'3ecf7a90 00007ffd'c1564f34 : 00000000'00000000 00000000'00000000 00000000'00000000 00000000'00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffb881'3ecf7b00) 5. Diese Ablaufverfolgung zeigt, dass der Thread, der die E/A ausgelöst hat, auf Abbruch oder Vervollständigung wartet. Der nächste Schritt besteht darin, wie in den früheren Experimenten den Erweiterungsbefehl !irp auf den IRP-Zeiger anzuwenden, um das Problem zu analysieren: lkd> !irp ffff8c0b84f1d130 Irp is active with 1 stacks 1 is current (= 0xffff8c0b84f1d200) No Mdl: No System Buffer: Thread ffff8c0b85377300: Irp stack trace. cmd flg cl Device File Completion-Context >[IRP_MJ_DEVICE_CONTROL(e), N/A(0)] 5 0 ffff8c0b886b5590 ffff8c0b88d2b550 00000000-00000000 \Driver\MYFAULT Args: 00000000 00000000 83360020 00000000 → E/A-Verarbeitung Kapitel 6 621
6. Diese Ausgabe zeigt klar, welcher Treiber der Schuldige ist, nämlich \Driver\MYFAULT bzw. Myfault.sys. Der Name des Treibers deutet schon an, dass eine solche Situation nur aufgrund eines Treiberproblems auftreten kann, nicht aufgrund einer fehlerhaften Anwendung. Obwohl Sie jetzt wissen, welcher Treiber das Problem verursacht hat, gibt es jedoch leider keine andere Lösungsmöglichkeit, als das System neu zu starten. Das ist notwendig, da Windows niemals davon ausgehen kann, dass sich der fehlende Abbruch gefahrlos ignorieren lässt. Das IRP könnte jederzeit zurückspringen und den Systemarbeitsspeicher beschädigen. Wenn Ihnen dieses Problem in der Praxis begegnet, sollten Sie nach einer neueren Version des Treibers Ausschau halten. Möglicherweise ist der Bug darin schon korrigiert. E/A-Vervollständigungsports Eine hochleistungsfähige Serveranwendung erfordert ein effizientes Threadmodell. Zu wenige, aber auch zu viele Serverthreads zur Verarbeitung von Clientanforderungen können Leistungsprobleme verursachen. Wenn ein Server nur einen einzigen Thread zur Handhabung sämtlicher Anforderungen erstellt, kann er immer nur eine Anforderung auf einmal verarbeiten, weshalb die Clients ausgehungert werden können. Ein einzelner Thread könnte zwar auch gleichzeitig mehrere Anforderungen verarbeiten, indem er bei Beginn der E/A-Operation zur nächsten umschaltet, doch eine solche Architektur ist sehr kompliziert und nutzt die Möglichkeiten von Systemen mit mehr als einem logischen Prozessor nicht richtig aus. Der andere Extremfall besteht darin, dass ein Server einen riesigen Threadpool erstellt, sodass jede Clientanforderung von einem eigenen Thread verarbeitet werden kann. Das führt jedoch dazu, dass viele Threads aufwachen, ein wenig auf der CPU arbeiten, dann blockiert werden, während sie auf E/A warten, und schließlich nach dem Abschluss der Verarbeitung wiederum blockiert werden, wenn sie auf eine neue Anforderung warten. Auf jeden Fall führen zu viele Threads zu einer erheblichen Menge von Kontextwechseln, da der Scheduler die Prozessorzeit auf die verschiedenen aktiven Threads aufteilen muss. Ein solches Verfahren lässt sich daher nicht gut skalieren. Ein Server sollte so wenig Kontextwechsel wie möglich durchführen, die unnötige Blockierung von Threads vermeiden und gleichzeitig die Parallelverarbeitung durch die Verwendung mehrerer Threads maximieren. Der Idealzustand besteht also darin, dass es auf jedem Prozessor einen Thread gibt, der aktiv eine Clientanforderung verarbeitet, und dass diese Threads nicht blockiert werden, wenn sie eine Anforderung abgeschlossen haben und auf weitere Anforderungen warten. Damit das funktioniert, muss die Anwendung jedoch eine Möglichkeit haben, einen anderen Thread zu aktivieren, wenn ein Thread, der eine Clientanforderung verarbeitet, durch E/A blockiert ist (z. B. wenn er im Rahmen der Verarbeitung in einer Datei liest). 622 Kapitel 6 Das E/A-System
Das IoCompletion-Objekt Als Sammelpunkt für den Abschluss von E/A-Operationen mit mehreren Dateihandles verwenden Anwendungen das Exekutivobjekt IoCompletion, das als Vervollständigungsport in die Windows-API exportiert wird. Wenn eine Datei mit einem solchen Port verknüpft ist, können jegliche für die Datei abgeschlossenen asynchronen E/A-Operationen dazu führen, dass ein Vervollständigungspaket in die Warteschlange für diesen Port gestellt wird. Ein Thread kann auf den Abschluss ausstehender E/A-Operationen für mehrere Dateien warten, indem er einfach darauf wartet, dass ein Vervollständigungspaket in die Warteschlange für den Vervollständigungsport gestellt wird. Mit der Funktion WaitForMultipleObjects bietet die Windows-API eine ähnliche Möglichkeit. Vervollständigungsports aber bieten einen wichtigen Vorteil, nämlich einen Parallelitätswert. Dabei handelt es sich um die Anzahl der Threads einer Anwendung, die aktiv Clientanforderungen handhaben. Festgelegt wird er mit der Hilfe des Systems. Wenn eine Anwendung einen Vervollständigungsport erstellt, gibt sie einen Parallelitätswert an, also die maximale Anzahl der mit diesem Port verknüpften Threads, die gleichzeitig laufen können. Wie bereits erwähnt, besteht der Idealzustand darin, dass auf jedem Prozessor des Systems jederzeit ein Thread aktiv ist. Windows nutzt die Parallelitätswerte der Ports, um zu steuern, wie viele aktive Threads eine Anwendung hat. Ist die Anzahl der aktiven Threads an einem Port gleich dem Parallelitätswert, dann wird einem Thread, auf den der Port wartet, die Ausführung nicht erlaubt. Stattdessen beendet ein aktiver Thread die Verarbeitung seiner laufenden Anforderung. Danach wird geprüft, ob ein weiteres Paket an dem Port wartet. Wenn ja, greift der Thread dieses Paket auf und verarbeitet es. Dabei erfolgt kein Kontextwechsel, und die Kapazitäten der CPUs werden nahezu vollständig ausgeschöpft. Verwendung von Vervollständigungsports Abb. 6–24 zeigt einen Überblick über die Funktionsweise von Vervollständigungsports. Erstellt wird ein solcher Port über einen Aufruf der Windows-API-Funktion CreateIoCompletionPort. Werden Threads an einem solchen Port blockiert, so werden sie mit ihm verknüpft und in ­LIFO-Reihenfolge (Last In, First Out) aufgeweckt. Es ist also der zuletzt blockierte Thread, der das nächste Paket bekommt. Die Stacks von Threads, die längere Zeit blockiert sind, können auf die Festplatte ausgelagert werden. Sind mit einem Port mehr Threads verknüpft, als Arbeit vorliegt, wird dadurch die Speichergröße der am längsten blockierten Threads reduziert. Eine Serveranwendung empfängt Clientanforderungen gewöhnlich über Netzwerkendpunkte, die durch Dateihandles bezeichnet werden, zum Beispiel Winsock2-Sockets und benannte Pipes. Während der Server seine Kommunikationsendpunkte erstellt, verknüpft er sie mit einem Vervollständigungsport und dessen Threads, die auf eingehende Anforderungen warten. Dazu ruft er GetQueuedCompletionStatus(Ex) für den Port auf. Wenn ein Thread ein Paket von dem Vervollständigungsport erhält, beginnt er, die Anforderung zu verarbeiten und wird dadurch ein aktiver Thread. Während dieser Verarbeitung wird der Thread mehrmals blockiert, etwa wenn er Daten in einer Datei auf der Festplatte lesen oder schreiben oder sich mit anderen Threads synchronisieren muss. Windows erkennt dann, dass an dem Vervollständigungsport ein Thread weniger aktiv ist. Steht dann ein weiteres Paket in der Warteschlange, so wird ein anderer Thread aufgeweckt, der an dem Vervollständigungsport wartet. E/A-Verarbeitung Kapitel 6 623
Eingehende Clientanforderung Vervollständigungsport Am Vervollständigungsport blockierte Threads CPU-Verarbeitung (aktiv) Datei-E/A – Blockierung (inaktiv) CPU-Verarbeitung (aktiv) Abbildung 6–24 Funktionsweise von E/A-Vervollständigungsports Nach den Richtlinien von Microsoft wird der Parallelitätswert ungefähr auf die Anzahl der Prozessoren im System gesetzt. Es ist jedoch möglich, dass die Anzahl der Threads an einem Vervollständigungsport den Parallelitätsgrenzwert übersteigt. Betrachten Sie dazu den Fall des Grenzwerts 1: 1. Eine Clientanforderung kommt herein. Ein Thread wird zur Verarbeitung der Anforderung abgestellt und wird aktiv. 2. Eine zweite Anforderung kommt herein, aber da der Parallelitätsgrenzwert bereits erreicht ist, darf ein zweiter Thread, der an dem Port wartet, nicht weitermachen. 3. Der erste Thread wartet auf eine Datei-E/A und wird dadurch inaktiv. 4. Der zweite Thread wird freigegeben. 5. Während der zweite Thread nach wie vor aktiv ist, wird die Datei-E/A des ersten Threads abgeschlossen, sodass er wieder aktiv wird. Bis einer der beiden Threads wieder blockiert wird, beträgt der Parallelitätswert 2, also mehr als der Grenzwert von 1. Meistens verbleibt die Anzahl der aktiven Threads jedoch am Grenzwert oder nur knapp darüber. Die API für Vervollständigungsports ermöglicht es einer Serveranwendung auch, privat definierte Vervollständigungspakete mit der Funktion PostQueuedCompletionStatus in eine Portwarteschlange zu stellen. Diese Funktion nutzen Server gewöhnlich, um ihre Threads über externe Ereignisse zu informieren, z. B. über die Notwendigkeit, sich ordnungsgemäß herunterzufahren. Anwendungen können die zuvor beschriebene threadagnostische E/A in Kombination mit Vervollständigungsports verwenden, damit sie die Threads mit einem Vervollständigungsportobjekt statt mit ihren eigenen E/A-Operationen verknüpfen können. Außer Skalierungsvorteilen bieten Vervollständigungsports auch den Vorteil einer Reduzierung von Kontextwechseln. Normale E/A-Abschlussvorgänge müssen von dem Thread ausgeführt werden, der die E/A-Operation ausgelöst hat, aber wenn eine mit einem Vervollständigungsport verknüpfte Operation abgeschlossen werden muss, kann der E/A-Manager einen beliebigen wartenden Thread dafür verwenden. 624 Kapitel 6 Das E/A-System
Funktionsweise von E/A-Vervollständigungsports Windows-Anwendungen erstellen Vervollständigungsports, indem sie die Windows-API C­ reateIoCompletionPort aufrufen und dabei einen NULL-Porthandle angeben. Dadurch wird der Systemdienst NtCreateIoCompletion ausgeführt. Das IoCompletion-Objekt der Exekutive enthält ein Kernelsynchronisiserungsobjekt, das als Kernelwarteschlange bezeichnet wird. Der Systemdienst erstellt also ein Vervollständigungsportobjekt und initialisiert im zugewiesenen Speicher des Ports ein Warteschlangenobjekt. (Ein Zeiger auf den Port verweist auch auf das Warteschlangenobjekt, da die Warteschlange das erste Element des Ports ist.) Der Parallelitätswert eines Kernelwarteschlangenobjekts wird festgelegt, wenn ein Thread das Objekt initialisiert. In diesem Fall wird der an CreateIoCompletionPort übergebene Wert verwendet. Um das Warteschlangenobjekt eines Ports zu initialisieren, ruft NtCreateIoCompletion die Funktion KeInitializeQueue auf. Wenn eine Anwendung CreateIoCompletionPort aufruft, um ein Dateihandle mit einem Port zu verknüpfen, wird der Systemdienst NtSetInformationFile mit dem Dateihandle als primärem Parameter ausgeführt. Als Informationsklasse wird FileCompletionInformation festgelegt, und das Handle des Vervollständigungsports sowie der Parameter CompletionKey von CreateIoCompletionPort sind die Datenwerte. NtSetInformationFile dereferenziert das Dateihandle, um das Dateiobjekt abzurufen, und weist eine Datenstruktur mit dem Vervollständigungskontext zu. Schließlich richtet NtSetInformationFile das Feld CompletionContext im Dateiobjekt so ein, dass es auf die Kontextstruktur zeigt. Wird eine asynchrone E/A-Operation für ein Dateiobjekt abgeschlossen, prüft der E/A-Manager, ob das Feld CompletionContext im Dateiobjekt nicht NULL ist. Wenn ja, weist er ein Vervollständigungspaket zu und stellt es in die Warteschlange für den Port, indem er KeInsertQueue mit dem Port als die zu verwendende Warteschlange aufruft. (Das funktioniert, da das Vervollständigungsport- und das Warteschlangenobjekt dieselbe Adresse haben.) Wenn ein Serverthread GetQueuedCompletionStatus aufruft, wird der Systemdienst NtRe­ moveIoCompletion ausgeführt. Nach der Validierung der Parameter und der Übersetzung des Porthandles in einen Zeiger auf den Port ruft NtRemoveIoCompletion die Funktion IoRemoveIoCompletion auf, die ihrerseits KeRemoveQueueEx aufruft. Bei Hochleistungsservern ist es möglich, dass mehrere E/A-Operationen abgeschlossen wurden. Der Thread wird zwar nicht blockiert, wendet sich aber immer noch jedes Mal an den Kernel, um ein Element abzurufen. Die API GetQueuedCompletionStatus bzw. GetQueuedCompletionStatusEx ermöglicht Anwendungen, mehr als einen E/A-Vervollständigungsstatus auf einmal abzurufen, was die Anzahl der Benutzer/ Kernel-Roundtrips verringert und für dauerhafte Spitzenleistung sorgt. Intern wird dies durch die Funktion NtRemoveIoCompletionEx implementiert. Sie ruft IoRemoveIoCompletion mit einem Zähler für Elemente in der Warteschlange auf, der an KeRemoveQueueEx übergeben wird. Wie Sie sehen, bilden KeRemoveQueueEx und KeInsertQueue die Engine hinter den Vervollständigungsports. Diese Funktionen bestimmen, ob ein Thread, der auf ein E/A-Vervollständigungspaket wartet, aktiviert werden soll. Intern führt ein Warteschlangenobjekt einen Zähler der zurzeit aktiven Threads sowie der Maximalzahl aktiver Threads. Wenn ein Thread KeRemoveQueueEx aufruft und die aktuelle Anzahl der Threads gleich dem Maximum ist oder dieses übersteigt, wird der Thread (in LIFO-Reihenfolge) auf eine Liste der Threads gesetzt, E/A-Verarbeitung Kapitel 6 625
die darauf warten, ein Vervollständigungspaket zu verarbeiten. Diese Threadliste hängt am Warteschlangenobjekt. Die Steuerblockdatenstruktur eines Threads (KTHREAD) enthält einen Zeiger, der auf das Warteschlangenobjekt der Warteschlange verweist, mit dem der Thread verknüpft ist. Ist der Zeiger NULL, dann gehört der Thread zu keiner Warteschlange. Mithilfe des Warteschlangenzeigers im Steuerungsblock merkt sich Windows, welche Threads dadurch inaktiv werden, dass sie auf etwas anderes warten als den Vervollständigungsport. Die Schedulerroutinen, die möglicherweise zur Blockierung eines Threads führen (wie KeWaitForSingleObject, KeDelayExecutionThread usw.), überprüfen den Warteschlangenzeiger des Threads. Wenn er nicht NULL ist, rufen diese Funktionen die Warteschlangenfunktion KiActivateWaiterQueue auf, die den Zähler der mit der Warteschlange verknüpften aktiven Threads dekrementiert. Ist die resultierende Zahl kleiner als das Maximum und steht mindestens ein Vervollständigungspaket in der Warteschlange, wird der Thread am Anfang der Threadliste in der Warteschlange aufgeweckt und erhält das älteste Paket. Wacht dagegen ein mit der Warteschlange verknüpfter Thread nach einer Blockierung auf, führt der Scheduler die Funktion KiUnwaitThread auf, die den Zähler aktiver Threads der Warteschlange heraufsetzt. Die Windows-API-Funktion PostQueuedCompletionStatus führt dazu, dass der Systemdienst NtSetIoCompletion ausgeführt wird. Diese Funktion fügt einfach das angegebene Paket mit KeInsertQueue in die Warteschlange des Vervollständigungsports ein. Abb. 6–25 zeigt ein Beispiel für ein Vervollständigungsportobjekt in Betrieb. Zwar sind beide Threads zur Verarbeitung von Vervollständigungspaketen bereit, aber aufgrund des Parallelitätswerts von 1 kann nur ein mit dem Port verknüpfter Thread aktiv sein, weshalb die beiden Threads blockiert sind. Wartende Threads Aktiver Thread Vervollstän­ digungspaket Vervollständigungsport Warte­schlange Parallelität: 1 Vervollstän­ digungspaket Datei­objekt Abbildung 6–25 Ein E/A-Vervollständigungsportobjekt in Betrieb Das Benachrichtigungsmodell eines E/A-Vervollständigungsports können Sie mit der API SetFileCompletionNotificationModes einstellen. Dadurch können Anwendungsentwickler die Vorteile von Verbesserungen nutzen, die gewöhnlich Codeänderungen erfordern, aber einen noch höheren Durchsatz bieten. Es gibt drei mögliche Benachrichtigungsmodi (siehe Tabelle 6–4). Sie gelten jeweils für ein Dateihandle und können nach der Festlegung nicht mehr geändert werden. 626 Kapitel 6 Das E/A-System
Modus Bedeutung Überspringen des Vervollständigungsports bei Erfolg (FILE_SKIP_COMPLETION_PORT_ON_SUCCESS=1) Wenn die folgenden drei Bedingungen wahr sind, stellt der E/A-Manager keinen Vervollständigungseintrag in die Warteschlange des Ports, auch wenn er es normalerweise tun würde. Erstens muss der Vervollständigungsport mit dem Dateihandle verknüpft sein. Zweitens muss die Datei für asynchrone E/A geöffnet sein. Drittens muss die Anforderung sofort erfolgreich zurückspringen und nicht ERROR_PENDING zurückgeben. Festgelegtes Ereignis für das Handle überspringen (FILE_SKIP_SET_EVENT_ON_HANDLE=2) Der E/A-Manager legt das Ereignis für das Dateiobjekt nicht fest, wenn eine Anforderung die Steuerung mit einem Erfolgscode zurückgibt oder wenn der zurückgegebene Fehler ERROR_PENDING ist und es sich bei der aufgerufenen Funktion nicht um eine synchrone Funktion handelt. Wurde für die Anforderung ein ausdrückliches Ereignis bereitgestellt, so wird es nach wie vor signalisiert. Festgelegtes Benutzerereignis bei schneller E/A überspringen (FILE_SKIP_SET_USER_EVENT_ON_FAST_IO=4) Der E/A-Manager legt das für die Anforderung bereitgestellte ausdrückliche Ereignis nicht fest, wenn die Anforderung den Weg für die schnelle E/A genommen hat und mit einem Erfolgscode zurückkehrt oder der zurückgegebene Fehler ERROR_PENDING ist und es sich bei der aufgerufenen Funktion nicht um eine synchrone Funktion handelt. Tabelle 6–4 Benachrichtigungsmodi für E/A-Vervollständigungsports E/A-Priorisierung Ohne E/A-Priorisierung würden Hintergrundaktivitäten wie Suchindizierung, Virusscans und Festplattendefragmentierung die Reaktivität von Vordergrundoperationen erheblich beeinträchtigen. Wenn ein Benutzer beispielsweise eine Anwendung startet oder ein Dokument öffnet, während ein anderer Prozess eine Festplatten-E/A durchführt, kommt es zu Verzögerungen, da der Vordergrundtask auf Festplattenzugriff wartet. Diese Störungen wirken sich auch auf die Streaming-Wiedergabe von Multimediainhalten wie Musik auf der Festplatte aus. In Windows gibt es zwei Arten der E/A-Priorisierung, mit denen E/A-Operationen im Vordergrund Vorrang erhalten: Priorität für einzelne E/A-Operationen und E/A-Bandbreitenreservierung. E/A-Prioritäten Der E/A-Manager von Windows bietet intern eine Unterstützung für fünf E/A-Prioritäten, wobei aber nur drei verwendet werden (siehe Tabelle 6–5). In zukünftigen Versionen von Windows werden Hoch und Niedrig möglicherweise ebenfalls unterstützt. E/A-Verarbeitung Kapitel 6 627
E/A-Priorität Verwendung Kritisch Speicher-Manager Hoch Nicht verwendet Normal Normale Anwendungs-E/A Niedrig Nicht verwendet Sehr niedrig Geplante Aufgaben, SuperFetch, Defragmentierung, Inhaltsindizierung, Hintergrundaktivitäten Tabelle 6–5 E/A-Prioritäten Die Standardpriorität ist Normal. Der Speicher-Manager verwendet Kritisch, wenn er bei knappem Speicher geänderte Daten auf die Festplatte schreiben möchte, um im RAM Platz für andere Daten und Code zu schaffen. Der Taskplaner von Windows setzt die E/A-Priorität für Aufgaben, die die Standardpriorität haben, auf Sehr niedrig. Anwendungen, die Hintergrundverarbeitung durchführen, vergeben ebenfalls die Priorität Sehr niedrig. Alle Windows-Hintergrundoperationen wie Scans von Windows Defender und die Indizierung für die Desktopsuche verwenden die Priorität Sehr niedrig. Priorisierungsstrategien Intern sind die fünf E/A-Prioritäten auf zwei E/A-Priorisierungsmodi aufgeteilt, die sogenannten Strategien: Hierarchie- und Leerlaufpriorisierung. Erstere umfasst alle Prioritäten außer Sehr niedrig. Sie setzt das folgende Verfahren um: QQ Alle E/A-Operationen mit kritischer Priorität müssen vor denen mit hoher Priorität ausgeführt werden. QQ Alle E/A-Operationen mit hoher Priorität müssen vor denen mit normaler Priorität ausgeführt werden. QQ Alle E/A-Operationen mit normaler Priorität müssen vor denen mit niedriger Priorität ausgeführt werden. QQ Alle E/A-Operationen mit niedriger Priorität werden nach denen mit irgendeiner höheren Priorität ausgeführt. Wenn eine Anwendung E/A-Operationen erstellt, werden die IRPs je nach ihrer Priorität in verschiedene E/A-Warteschlangen eingereiht. Die Hierarchiestrategie bestimmt die Reihenfolge der Operationen. Die Leerlaufstrategie dagegen verwendet eine eigene Warteschlange für E/A-Operationen mit Leerlaufpriorität. Da das System sämtliche E/A-Operationen mit Hierarchiepriorität vor jeglicher Leerlauf-E/A verarbeitet, ist es möglich, dass E/A-Operationen in dieser Warteschlange verhungern, solange es noch eine einzige E/A-Operation mit einer anderen als der Leerlaufpriorität in der Hierarchiewarteschlange gibt. Um eine solche Situation zu vermeiden und um den Backoff zu steuern (die Senderate der E/A-Übertragungen), wird die Warteschlange bei der Leerlaufstrategie mit einem Timer überwacht, um zu garantieren, dass wenigstens eine E/A-Operation pro Zeiteinheit (gewöhnlich 628 Kapitel 6 Das E/A-System
eine halbe Sekunde) verarbeitet wird. Werden Daten mit einer anderen als der Leerlaufpriorität geschrieben, kann der Cache-Manager die Änderungen sofort auf die Festplatte schreiben, anstatt dies auf später zu verschieben, und damit das vorausschauende Lesen für Leseopera­ tionen umgehen, die sonst vorab in der betreffenden Datei lesen würden. Außerdem wartet die Priorisierungsstrategie nach dem Abschluss des letzten Nicht-Leerlauf-E/A 50 ms lang, um die nächste Leerlauf-E/A auszugeben, da Leerlauf-E/A-Vorgänge mitten in Nicht-Leerlauf-Streams auftreten könnten, was kostspielige Suchvorgänge hervorrufen würde. Wenn wir all diese Strategien zur Veranschaulichung zu einer virtuellen globalen E/A-­ Warteschlange kombinieren, kann eine Momentaufnahme dieser Warteschlange wie in Abb. 6–26 aussehen. Beachten Sie, dass in jeder Warteschlange eine FIFO-Reihenfolge gilt (First In, First Out). Die Reihenfolge der einzelnen Vorgänge in der Abbildung ist nur ein Beispiel. Kritisch Hoch Hierarchie Normal Niedrig Sehr niedrig Leerlauf Abbildung 6–26 Beispieleinträge in einer globalen E/A-Warteschlange Benutzermodusanwendungen können die E/A-Priorität in drei verschiedenen Objekten einstellen. Die Funktionen SetPriorityClass (mit dem Wert PROCESS_MODE_BACKGROUND_BEGIN) und SetThreadPriority (mit dem Wert THREAD_MODE_BACKGROUND_BEGIN) legen die Priorität für alle E/A-Operationen fest, die vom gesamten Prozess bzw. einzelnen Threads generiert werden (wobei die Priorität jeweils in den IRPs der einzelnen Anforderungen gespeichert wird). Diese Funktionen wirken sich nur auf den aktuellen Prozess bzw. Thread aus und senken die E/A-Priorität auf Sehr niedrig. Darüber hinaus setzen sie auch die Planungspriorität auf 4 und die Speicherpriorität auf 1 herab. Die Funktion SetFileInformationByHandle kann die Priorität für ein einzelnes Dateiobjekt festlegen (wobei die Priorität im Dateiobjekt gespeichert wird). Treiber haben die Möglichkeit, die E/A-Priorität mit IoSetIoPriorityHint direkt in einem IRP einzustellen. Die Felder für die E/A-Priorität in IRPs und Dateiobjekten sind lediglich Hinweise (Hints). Es ist nicht garantiert, dass diese Einstellung respektiert wird, und nicht einmal sicher, dass sie von den einzelnen Treibern im Speicherstack überhaupt unterstützt wird. Die beiden Priorisierungsstrategien werden durch zwei verschiedene Arten von Treibern implementiert. Die Hierarchiestrategie wird durch Speicherporttreiber umgesetzt, die jeweils für die gesamte E/A an einem bestimmten Port zuständig sind, z. B. ATA, SCSI oder USB. Nur der ATA-Porttreiber (Ataport.sys) und der USB-Porttreiber (Usbstor.sys) implementieren jedoch diese Strategie; der SCSI- und der Speicherporttreiber (Scsiport.sys und Storport.sys) dagegen nicht. E/A-Verarbeitung Kapitel 6 629
Alle Porttreiber prüfen ausdrücklich, ob E/A-Operationen mit kritischer Priorität vorliegen, und verschieben sie an die Spitze ihrer Warteschlangen, selbst wenn sie den Hierarchiemechanismus nicht komplett unterstützen. Dies dient dazu, kritische Auslagerungsoperationen des Speicher-Managers zu fördern, um die Zuverlässigkeit des Systems zu garantieren. Massenspeichergeräte für Endverbraucher wie IDE- oder SATA-Festplatten und USB-Sticks nutzen daher die Vorteile der E/A-Priorisierung, SCSI-, Fibre-Channel- und iSCSI-Geräte dagegen nicht. Dagegen wird die Leerlaufstrategie vom Systemgerätetreiber für die Speicherklasse (Class­ pnp.sys) durchgesetzt, sodass diese Strategie automatisch auf E/A-Vorgänge an allen Speichergeräten einschließlich SCSI-Laufwerken angewendet wird. Damit ist sichergestellt, dass Leerlauf-E/A-Operationen dem Backoffalgorithmus unterliegen, damit das System auch bei einem starken Aufkommen von Leerlauf-E/A zuverlässig arbeitet und die Anwendungen, die diese Art der E/A durchführen, Fortschritte machen können. Durch die Implementierung dieser Strategie in einem von Microsoft bereitgestellten Klassentreiber werden auch Leistungsprobleme vermieden, die durch mangelnde Unterstützung in veralteten Drittanbieter-Porttreibern verursacht werden könnten. Abb. 6–27 gibt eine vereinfachte Darstellung des Speicherstacks und zeigt, wo die Strategien jeweils implementiert sind. Weitere Informationen über den Speicherstack erhalten Sie in Kapitel 12 von Band 2. Benutzermodus Kernelmodus Anwendung Dateisystem Volume/Partition Geräteklasse Warteschlange für Leerlaufpriorität Befehlsport Warteschlange für Hierarchiepriorität E/A-Bandbreitenreservierung Speicher Abbildung 6–27 Implementierung der E/A-Priorisierungsstrategien im Speicherstack Prioritätsumkehrung verhindern Um eine Umkehrung der E/A-Priorität zu verhindern, bei der ein Thread mit hoher Priorität von einem Thread mit niedriger Priorität ausgehungert wird, wendet der Sperrmechanismus der Exekutivressource (ERESOURCE) verschiedene Strategien an. Diese Ressource wurde eigens für die Implementierung der E/A-Prioritätsvererbung ausgewählt, da sie häufig in Dateisystem- und 630 Kapitel 6 Das E/A-System
Speichertreibern verwendet wird, wo die meisten Probleme mit Prioritätsumkehrung auftreten können. (Mehr über die Exekutivressource erfahren Sie in Kapitel 8 von Band 2.) Erwirbt ein Thread mit niedriger E/A-Priorität eine Exekutivressource und warten bereits andere Threads mit normaler oder höherer Priorität auf diese Ressource, so wird der aktuelle Thread mithilfe der API PsBoostThreadIo, die den Zähler IoBoostCount in der ETHREAD-Struktur inkrementiert, vorübergehend auf normale E/A-Priorität heraufgesetzt. Sie benachrichtigt auch Autoboost, wenn die E/A-Priorität des Threads erhöht oder die Erhöhung zurückgenommen wird. (Mehr über Autoboost erfahren Sie in Kapitel 4.) Anschließend ruft sie die API IoBoostThreadIoPriority auf, die alle IRPs in der Warteschlange des Zielthreads auflistet (denken Sie daran, dass jeder Thread über eine Liste ausstehender IRPs verfügt) und prüft, welche davon eine niedrigere als die Zielpriorität haben (in diesem Fall normal). Dadurch werden die ausstehenden IRPs mit Leerlaufpriorität bestimmt. Anschließend werden die für diese IRPs jeweils verantwortlichen Geräteobjekte ermittelt. Der E/A-Manager prüft, ob ein Prioritätscallback registriert worden ist. Eine solche Registrierung können Treiberentwickler mit IoRegisterPriorityCallback und durch Setzen des Flags DO_­ PRIORITY_CALLBACK_ENABLED im Geräteobjekt vornehmen. Je nachdem, ob es sich um ein IRP für Auslagerungs-E/A handelt oder nicht, wird der Mechanismus für die Prioritätserhöhung Paging Boost (Auslagerungsschub) oder Threaded Boost (Threadschub) genannt. Wenn keine übereinstimmenden IRPs gefunden wurden, der Thread aber über ausstehende IRPs verfügt, erhalten sie alle unabhängig vom Geräteobjekt oder ihrer Priorität einen Prioritätsschub. Dies wird als Blanket Boost (Pauschalschub) bezeichnet. Prioritätsschübe und -bumps Um Aushungern, Umkehrung und andere unerwünschte Situationen im Zusammenhang mit der E/A-Priorität zu verhindern, verwendet Windows noch einige andere feine Modifizierungen der normalen E/A-Vorgänge. Gewöhnlich wird dabei die E/A-Priorität bei Bedarf erhöht. Das geschieht in folgenden Situationen: QQ Wenn ein Treiber mit einem IRP für ein bestimmtes Dateiobjekt aufgerufen wird und die Anforderung vom Kernelmodus kommt, sorgt Windows dafür, dass das IRP auch dann die normale Priorität verwendet, wenn das Objekt einen niedrigeren Prioritätshinweis hat. Dies wird als Kernelbump bezeichnet. QQ Bei Lese- oder Schreibvorgängen in der Auslagerungsdatei (durch IoPageRead bzw. IoPageWrite) prüft Windows, ob die Anforderung vom Kernelmodus stammt und nicht im Namen von SuperFetch erfolgt (da SuperFetch ausschließlich Leerlauf-E/A durchführt). Wenn ja, wird das IRP auch dann mit normaler Priorität ausgeführt, wenn der aktuelle Thread eine niedrigere E/A-Priorität hat. Dies wird als Auslagerungsbump bezeichnet. Das folgende Experiment zeigt ein Beispiel für die Priorität Sehr niedrig und führt vor, wie Sie sich E/A-Prioritäten für verschiedene Anforderungen in Process Monitor ansehen können. E/A-Verarbeitung Kapitel 6 631
Experiment: Durchsatz von Threads mit sehr niedriger und normaler E/A-Priorität Mit der im Begleitmaterial zu diesem Buch enthaltenen Beispielanwendung IO Priority können Sie sich den Unterschied des Durchsatzes von Threads mit unterschiedlichen E/A-Prioritäten ansehen. Gehen Sie dazu wie folgt vor: 1. Starten Sie IoPriority.exe. 2. Aktivieren Sie in dem Dialogfeld im Bereich Thread 1 das Kontrollkästchen Low Priority. 3. Klicken Sie auf Start I/O. Jetzt können Sie einen erheblichen Geschwindigkeitsunterschied zwischen den beiden Threads beobachten, wie der folgende Screenshot zeigt: Wenn beide Threads auf niedriger Priorität laufen und sich das System größtenteils im Leerlauf befindet, entspricht ihr Durchsatz ungefähr dem eines einzelnen Threads mit normaler Priorität, da Threads mit niedriger Priorität nicht künstlich gedrosselt oder auf andere Weise aufgehalten werden, wenn es keine Konkurrenz durch Threads mit höherer Priorität gibt. 4. Öffnen Sie den Prozess in Process Explorer und schauen Sie sich den Thread mit der niedrigen Priorität an: → 632 Kapitel 6 Das E/A-System
5. In Process Monitor können Sie auch die E/A-Operationen von IO Priority verfolgen und sich ihre Prioritätshinweise ansehen. Richten Sie dazu in Process Monitor einen Filter für IoPriority.exe ein und wiederholen Sie das Experiment. Die einzelnen Threads dieser Anwendung lesen eine Datei, deren Name sich aus _File_ und der Thread-ID zusammensetzt. 6. Scrollen Sie nach unten bis zu einem Schreibvorgang in _File_1. Dabei sollten Sie folgende Ausgabe sehen: 7. In dem Screenshot haben die E/A-Operationen für die Datei _FILE_7920 eine sehr niedrige Priorität. In den Spalten Time of Day und Relative Time können Sie außerdem erkennen, dass diese E/A-Vorgänge jeweils eine halbe Sekunde auseinanderliegen. Dies ist ein weiterer Hinweis darauf, dass die Leerlaufstrategie ausgeführt wird. Experiment: Analysieren der Leistung bei Prioritätsschüben und -bumps Der Kernel führt mehrere interne Variablen, die Sie durch die nicht dokumentierte, in NtQuerySystemInformation vorhandene Systemklasse SystemLowPriorityIoInformation abfragen können. Sie können sich diese Zahlen auf Ihrem System aber auch mit dem lokalen Kerneldebugger ansehen, ohne eine solche Anwendung zu schreiben oder zu nutzen. Die folgenden Variablen stehen zur Verfügung: QQ IoLowPriorityReadOperationCount und IoLowPriorityWriteOperationCount QQ IoKernelIssuedIoBoostedCount QQ IoPagingReadLowPriorityCount und IoPagingWriteLowPriorityCount QQ IoPagingReadLowPriorityBumpedCount und IoPagingWriteHighPriorityBumpedCount QQ IoBoostedThreadedIrpCount und IoBoostedPagingIrpCount QQ IoBlanketBoostCount Mit dem Speicherauszugsbefehl dd des Kerneldebuggers können Sie sich die Werte dieser Variablen ansehen. (Es sind alles 32-Bit-Werte.) E/A-Verarbeitung Kapitel 6 633
Bandbreitenreservierung (geplante Datei-E/A) Die Unterstützung von Windows für die E/A-Bandbreitenreservierung ist sehr hilfreich für Anwendungen, die einen gleichmäßigen E/A-Durchsatz benötigen. Beispielsweise kann eine Mediaplayeranwendung das E/A-System mit einem Aufruf von SetFileBandwidthReservation bitten, ihr die Möglichkeit zu gewähren, Daten mit einer festgelegten Rate von einem Gerät zu lesen. Wenn das Gerät die Daten mit der angeforderten Rate liefern kann und die bestehenden Reservierungen es erlauben, teilt das E/A-System der Anwendung mit, wie schnell sie E/A-­ Operationen ausgeben kann und wie umfangreich diese Operationen sein dürfen. Das E/A-System führt andere E/A-Vorgänge nur dann aus, wenn es die Anforderungen der Anwendungen, die Reservierungen für das Zielspeichergerät vorgenommen haben, erfüllen kann. Abb. 6–28 zeigt den prinzipiellen zeitlichen Verlauf von E/A-Vorgängen in derselben Datei. Die grauen Abschnitte sind die einzigen, die für andere Anwendungen zur Verfügung stehen. Wenn die E/A-Bandbreite belegt ist, müssen neue E/A-Operationen bis zum nächsten Zyklus warten. Reservierte Bandbreite Freie Band­ breite Reservierte Bandbreite Reservierte Bandbreite Freie Band­ breite Reservierte Bandbreite Abbildung 6–28 Bandbreitenreservierung für E/A-Anforderungen Wie die Hierarchiestrategie wird die Bandbreitenreservierung in den Porttreibern implementiert. Dadurch steht sie nur für IDE-, SATA- und USB-Massenspeichergeräte zur Verfügung. Containerbenachrichtigungen Containerbenachrichtigungen sind besondere Klassen von Ereignissen, die Treiber für einen asynchronen Callbackmechanismus registrieren können. Dazu verwenden sie die API IoRegisterContainerNotification und wählen die gewünschte Benachrichtigungsklasse aus. Bis jetzt ist nur eine solche Klasse in Windows implementiert, nämlich IoSessionStateNotification. Damit können Treiber ihre registrierten Callbacks aufrufen lassen, wenn in einer gegebenen Sitzung eine Statusänderung festgestellt wird. Folgende Änderungen werden erkannt: QQ Eine Sitzung wird erstellt oder beendet. QQ Ein Benutzer stellt eine Verbindung zu der Sitzung her oder trennt sie. QQ Ein Benutzer meldet sich an einer Sitzung an oder ab. 634 Kapitel 6 Das E/A-System
Bei Angabe eines Geräteobjekts für eine bestimmte Sitzung kann der Treibercallback nur für diese Sitzung aktiv werden. Wird dagegen ein globales Geräteobjekt angegeben (oder gar keines), so empfängt der Treiber Benachrichtigungen für alle Ereignisse im System. Das ist besonders für Geräte nützlich, die an dem durch die Terminaldienste bereitgestellten Plug-&-Play-Geräteumleitungsmechanismus beteiligt sind. Er macht es möglich, dass ein Remotegerät auch im Bus des PnP-Managers des Hosts zu sehen ist, der die Verbindung aufnimmt (z. B. in Form von Audio- oder Druckerumleitung). Wenn ein Benutzer beispielsweise die Verbindung zu einer Sitzung mit Audiowiedergabe aufhebt, muss der Gerätetreiber benachrichtigt werden, damit er die Umleitung des Quellaudiostreams beendet. Treiberüberprüfung Die Treiberüberprüfung (Driver Verifier) ist ein Mechanismus, mit dem sich gängige Bugs in Gerätetreibern und anderem Kernelmodus-Systemcode finden und isolieren lassen. Microsoft setzt die Treiberüberprüfung selbst ein, um sowohl eigene als auch von den Herstellern für die WHQL-Prüfung eingereichte Gerätetreiber zu untersuchen. Dadurch ist sichergestellt, dass die eingereichten Treiber mit Windows kompatibel sind und keine der üblichen Treiberfehler aufweisen. (Es gibt auch ein Werkzeug zur Anwendungsüberprüfung, das zu erheblichen Qualitätsverbesserungen von Benutzermoduscode in Windows geführt hat, allerdings werden wir es in diesem Buch nicht behandeln.) Die Treiberüberprüfung ist hauptsächlich als Werkzeug gedacht, mit dem die Entwickler von Gerätetreibern Bugs in ihrem Code aufspüren können. Es ist jedoch auch sehr nützlich für Systemadministratoren, um Abstürze zu analysieren. Kapitel 15 beschreibt die Anwendung der Treiberüberprüfung für die Absturzanalyse. Die Treiberüberprüfung ist auf mehrere Systemkomponenten verteilt. Im Speicher-Manager, im E/A-Manager und in der HAL gibt es Treiberüberprüfungsoptionen, die Sie aktivieren können. Konfiguriert werden sie mit dem Treiberüberprüfungs-Manager (%SystemRoot%\System32\ Verifier.exe). Wenn Sie ihn ohne Befehlszeilenargumente ausführen, wird eine Assistentenoberfläche wie in Abb. 6–29 angezeigt. (Sie können den Treiberüberprüfungs-Manager auch an der Befehlszeile aktivieren und deaktivieren und seine aktuellen Einstellungen einsehen.) Um sich die möglichen Schalter anzeigen zu lassen, geben Sie an einer Befehlszeile verifier /? ein. Treiberüberprüfung Kapitel 6 635
Abbildung 6–29 Der Treiberüberprüfungs-Manager Der Treiberüberprüfungs-Manager unterscheidet zwischen Standard- und Zusatzeinstellungen. Die Grenze ist etwas willkürlich gezogen, allerdings umfassen die Standardeinstellungen die gängigeren Optionen, die am besten bei jedem zu testenden Treiber verwendet werden sollen, während es sich bei den Zusatzeinstellungen um weniger gebräuchliche oder um solche handelt, die nur für bestimmte Arten von Treibern sinnvoll sind. Wenn Sie in der Hauptseite des Assistenten Benutzerdefinierte Einstellungen erstellen wählen, werden alle Optionen in einer Spalte angezeigt, wobei angegeben ist, welche davon Standard- und welche Zusatzeinstellungen sind (siehe Abb. 6–30). Unabhängig davon, welche Optionen Sie ausgewählt haben, überwacht die Treiberüberprüfung stets die zur Überprüfung ausgewählten Treiber und sucht dabei nach verschiedenen unzulässigen und grenzwertigen Operationen, z. B. nach dem Aufruf von Kernelpoolfunktionen auf ungültigen IRQLs, nach doppelter Freigabe von Speicher, nicht ordnungsgemäßer Freigabe von Spinlocks, mangelnder Freigabe von Timern, Verweise auf freigegebene Objekte, Verzögerung des Herunterfahrens um mehr als 20 Minuten und die Anforderung von Speicherzuweisungen der Größe 0. 636 Kapitel 6 Das E/A-System
Abbildung 6–30 Einstellungen der Treiberüberprüfung Die Einstellungen der Treiberüberprüfung werden unter dem Registrierungsschlüssel HKLM\ SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management gespeichert. Der Wert VerifyDriverLevel enthält eine Bitmaske, die die aktivierten Überprüfungsoptionen angibt, und der Wert VerifyDrivers die Namen der zu überwachenden Treiber. (Diese Werte erscheinen erst in der Registrierung, wenn Sie die Treiber im Treiberüberprüfungs-Manager auswählen.) Wenn Sie festgelegt haben, dass Sie alle Treiber überprüfen wollen (was Sie nicht tun sollten, da das System dadurch beträchtlich verlangsamt wird), wird VerifyDrivers auf das Zeichen * gesetzt. Je nachdem, welche Einstellungen Sie vorgenommen haben, kann es sein, dass Sie das System neu starten müssen, damit die ausgewählte Überprüfung vorgenommen werden kann. Sehr früh im Bootvorgang liest der Speicher-Manager die Registrierungswerte der Treiberüberprüfung, um zu bestimmen, welche Treiber untersucht und welche Optionen aktiviert werden sollen. (Beim Start im abgesicherten Modus werden die Einstellungen für die Treiberüberprüfung ignoriert.) Anschließend gleicht der Kernel die Namen aller in den Speicher geladenen Gerätetreiber mit der Liste der Treiber ab, die Sie zur Überprüfung ausgewählt haben. Für jeden Treiber, für den der Kernel eine Übereinstimmung findet, ruft er die Funktion VfLoadDriver auf, die wiederum andere interne Vf*-Funktionen aufruft, um die Verweise des Treibers auf eine Reihe von Kernelfunktionen durch Verweise auf die entsprechenden Versionen der Treiber­ überprüfung zu ersetzen. Beispielsweise wird ExAllocatePool durch VerifierAllocatePool ersetzt. Der Fenstersystemtreiber (Win32k.sys) führt ähnliche Änderungen durch, um für manche Funktionen die Versionen der Treiberüberprüfung zu nutzen. Treiberüberprüfung Kapitel 6 637
E/A-Überprüfungsoptionen Es gibt folgende Überprüfungsoptionen im Zusammenhang mit der E/A: QQ E/A-Überprüfung Bei der Auswahl dieser Option weist der E/A-Manager IRPs für die zu überprüfenden Treiber aus einem besonderen Pool zu und verfolgt ihre Nutzung. Außerdem lässt die Treiberüberprüfung das System abstürzen, wenn ein IRP abgeschlossen wird, der einen ungültigen Status enthält, oder wenn ein ungültiges Geräteobjekt an den E/A-Manager übergeben wird. Bei dieser Option werden des Weiteren alle IRPs überwacht, um sicherzustellen, dass die Treiber sie korrekt kennzeichnen, wenn sie sie asynchron abschließen, dass sie die Gerätestackpositionen korrekt verwalten und dass sie Geräte­objekte nur einmal löschen. Außerdem belastet die Treiberüberprüfung Treiber nach dem Zufallsprinzip, indem sie ihnen gefälschte Energieverwaltungs- und WMI-IRPs sendet, die Reihenfolge ändert, in der die Treiber aufgelistet werden, und den Status von PnP- und Energie-IRPs bei ihrem Abschluss anpasst. Das dient dazu, Treiber ausfindig zu machen, die einen falschen Status von ihren Dispatchroutinen zurückgeben. Darüber hinaus erkennt die Treiberüberprüfung es auch, wenn Sperren fälschlicherweise neu initialisiert werden, obwohl sie aufgrund der ausstehenden Entfernung eines Geräts noch gehalten werden. QQ DMA-Überprüfung DMA (Direct Memory Access, also »direkter Speicherzugriff«) ist ein von der Hardware unterstützter Mechanismus, der es Geräten erlaubt, Daten unter Umgehung der CPU in den oder aus dem physischen Speicher zu übertragen. Der E/A-Manager bietet mehrere Funktionen, mit denen Treiber DMA-Operationen auslösen und steuern können. Bei dieser Option wird die korrekte Nutzung dieser Funktionen und der vom E/A-Manager für DMA-Operationen bereitgestellten Puffer überprüft. QQ Ausstehende E/A-Anforderungen erzwingen Bei vielen Geräten werden asynchrone E/A-Operationen sofort abgeschlossen. Daher kann es sein, dass die Treiber nicht korrekt geschrieben wurden, um gelegentliche asynchrone E/As ordnungsgemäß zu handhaben. Ist diese Option aktiviert, gibt der E/A-Manager nach dem Zufallsprinzip STATUS_PENDING zurück, wenn ein Treiber IoCallDriver aufruft, um den asynchronen Abschluss der E/A zu provozieren. QQ IRP-Protokollierung Bei dieser Option wird die Verwendung von IRPs durch den Treiber überwacht und als WMI-Information aufgezeichnet. Anschließend können Sie mit dem WDK-Programm Dc2wmiparser.exe diese WMI-Datensätze in eine Textdatei umwandeln. Allerdings können für jedes Gerät nur zwanzig IRPs aufgezeichnet werden – bei jedem weiteren IRP wird der älteste vorhandene Eintrag überschrieben. Nach einem Neustart werden die Informationen verworfen, weshalb Sie Dc2wmiparser.exe vorher ausführen müssen, wenn Sie den Inhalt der Ablaufverfolgung später noch analysieren wollen. Speicherüberprüfungsoptionen Die Treiberüberprüfung bietet die folgenden Überprüfungsoptionen für den Arbeitsspeicher. Einige davon stehen auch in Beziehung mit E/A-Operationen. 638 Kapitel 6 Das E/A-System
Spezieller Pool Bei der Auswahl der Option Spezieller Pool werden Poolzuweisungen zwischen ungültige Seiten eingeschlossen, sodass Verweise vor oder hinter die Zuweisungen zu einer Kernelmodus-Zugriffsverletzung führen und das System zum Absturz bringen, wobei der fehlerhafte Treiber deutlich gemacht wird. Bei dieser Option erfolgen auch einige zusätzliche Überprüfungen, wenn ein Treiber Speicher zuweist oder freigibt. Außerdem weisen die Poolzuweisungsroutinen hierbei eine besondere Region des Kernelspeichers für die Verwendung durch die Treiberüberprüfung zu. Die Treiberüberprüfung leitet Speicherzuweisungsanforderungen von überwachten Treibern an diesen besonderen Poolbereich weiter, anstatt die regulären Kernelmoduspools zu verwenden. Wenn ein Gerätetreiber Speicher aus diesem besonderen Pool zuweist, rundet die Treiberüberprüfung diese Zuweisung auf eine gerade Anzahl von Seiten auf. Da sie ungültige Seiten vor und hinter die zugewiesenen Seiten stellt, greift ein Treiber auf eine ungültige Seite zu, wenn er versucht, außerhalb des Puffers zu lesen oder zu schreiben, sodass der Speicher-Manager eine Kernelmodus-Zugriffsverletzung auslöst. Abb. 6–31 zeigt ein Beispiel für den besonderen Pool, den die Treiberüberprüfung Gerätetreibern zuweist, um Überlauffehler feststellen zu können. Ungültige Seite Treiberpuffer Zufällige Daten Ungültige Seite Seite 2 (hohe Adresse) Seite 1 Seite 0 (niedrige Adresse) Abbildung 6–31 Aufbau von Zuweisungen aus dem besonderen Pool Die Treiberüberprüfung führt standardmäßig eine Untersuchung auf Überläufe durch. Dazu platziert sie den vom Gerätetreiber verwendeten Puffer am Ende der zugewiesenen Seite und füllt den Seitenanfang mit zufälligen Daten. Eine Untersuchung auf Unterläufe können Sie zwar nicht im Treiberüberprüfungs-Manager einstellen, aber dadurch, dass Sie den DWORD-Registrierungswert PoolTagOverruns im Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management auf 0 setzen. (Eine weitere Möglichkeit besteht darin, das Dienstprogramm Gflags.exe auszuführen und im Abschnitt Kernel Special Pool Tag die Option Verify Start statt der Standardoption Verify End auszuwählen.) Für die Untersuchung auf Unterläufe weist die Treiberüberprüfung den Treiberpuffer am Anfang statt am Ende der Seite zu. Die Überlauferkennung schließt jedoch auch einige Maßnahmen für die Unterlauferkennung ein. Wenn der Treiber seinen Puffer freigibt, um den Speicher an die Treiberüberprüfung zurückzugeben, vergewissert diese sich, dass das dem Puffer vorausgehende Muster unverändert ist. Wurde es geändert, so hat der Gerätetreiber den Puffer unterlaufen und in den Speicher außerhalb des Puffers geschrieben. Bei Zuweisungen aus dem besonderen Pool wird auch geprüft, ob die Zuweisung und Aufhebung der Zuweisung auf einer gültigen IRQL stattfinden. Dadurch kann ein Fehler erkannt Treiberüberprüfung Kapitel 6 639
werden, den manche Gerätetreiber machen: Sie weisen auslagerungsfähigen Speicher auf der DPC- oder Dispatch-IRQL oder höher zu. Den besonderen Pool können Sie auch manuell einrichten, indem Sie im Schlüssel HKLM\ SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management den DWORD-Wert PoolTag für die Zuweisungstags hinzufügen, die das System für den besonderen Pool nutzt. Wenn die Treiberüberprüfung nicht zur Überwachung eines bestimmten Gerätetreibers eingerichtet ist, der Treiber den zugewiesenen Speicher mit dem Tag aus dem Wert PoolTag verknüpft, dann wird der Speicher trotzdem aus dem besonderen Pool zugewiesen. Sie können den Wert von PoolTag auch auf 0x2a oder * setzen, sodass der gesamte Speicher, den der Treiber zuweist, aus dem besonderen Pool genommen wird, sofern ausreichend virtueller und physischer Speicher vorhanden ist. (Wenn es nicht genügend freie Seiten gibt, nehmen die Treiber die Zuweisung wieder aus dem regulären Pool vor.) Poolnachverfolgung Wenn die Poolnachverfolgung aktiviert ist, prüft der Speicher-Manager beim Entladen des Treibers, ob der Treiber alle von ihm vorgenommenen Speicherzuweisungen freigegeben hat. Wenn nicht, stürzt das System ab, wobei der fehlerhafte Treiber als Ursache angegeben wird. Die Treiberüberprüfung zeigt auch allgemeine Poolstatistiken auf der Registerkarte Poolnachverfolgung des Treiberüberprüfungs-Managers an (die angezeigt wird, wenn Sie auf der Hauptseite des Assistenten die Option Informationen über verifizierte Treiber anzeigen wählen und dann zweimal auf Weiter klicken). Sie können auch den Kerneldebuggerbefehl !verifier verwenden. Er zeigt mehr Informationen an als die Treiberüberprüfung und ist für Treiberautoren sehr hilfreich. Die Poolnachverfolgung und der besondere Pool decken nicht nur ausdrückliche Zuweisungsaufrufe wie ExAllocatePoolWithTag ab, sondern auch Aufrufe anderer Kernel-APIs, die implizit Speicher aus den Pools zuweisen: IoAllocateMdl, IoAllocateIrp und andere IRP-Zuweisungsaufrufe, verschiedene Rtl-String-APIs sowie IoSetCompletionRoutineEx. Eine weitere Treiberüberprüfungsfunktion, die durch die Option Poolnachverfolgung aktiviert wird, hängt mit Poolkontingenten zusammen. Bei einem Aufruf von ExAllocatePoolWithQuotaTag wird die Anzahl der zugewiesenen Bytes auf das Poolkontingent des aktuellen Threads angerechnet. Erfolgt ein solcher Aufruf von einer DPC-Routine aus, lässt sich aber nicht sagen, auf welchen Prozess die Zuweisung angerechnet werden soll, da DPC-Routinen im Kontext eines beliebigen Prozesses ausgeführt werden können. Bei der Option Poolnachverfolgung wird geprüft, ob diese Routine im Kontext von DPC-Routinen aufgerufen wird. Die Treiberüberprüfung kann auch eine Seitenverfolgung im verriegelten Speicher durchführen, bei der zusätzlich ermittelt wird, ob Seiten nach dem Abschluss einer E/A-Opera­ tion verriegelt geblieben sind. Wenn ja, wird statt PROCESS_HAS_LOCKED_PAGES der Absturzcode ­DRIVER_LEFT_LOCKED_PAGES_IN_PROCESS angegeben, der den für den Fehler verantwortlichen Treiber und die für die Verriegelung der Seiten verantwortliche Funktion nennt. 640 Kapitel 6 Das E/A-System
IRQL-Überprüfung Einer der am häufigsten vorkommenden Gerätetreiberbugs tritt auf, wenn ein Treiber auf auslagerungsfähige Daten oder auslagerungsfähigen Code zugreift, während der Prozessor, auf dem der Treiber ausgeführt wird, auf einer erhöhten IRQL arbeitet. Bei einer IRQL von DPC/ Dispatch-Ebene oder höher kann der Speicher-Manager aber keinen Seitenfehler handhaben. Wenn sich die Daten, auf die der Treiber zugreift, gerade im physischen Speicher befinden, erkennt das System den Fehler nicht einmal. Sind die Daten dagegen ausgelagert, kommt es zu einem Systemabsturz mit dem Stoppcode IRQL_NOT_LESS_OR_EQUAL (das heißt, die IRQL war nicht kleiner oder gleich der Ebene, die für die versuchte Operation erforderlich war, in diesem Fall für den Zugriff auf auslagerungsfähigen Speicher). Es ist gewöhnlich ziemlich schwierig, Gerätetreiber auf Bugs dieser Art zu untersuchen, doch die Treiberüberprüfung macht es einfach. Wenn Sie die Option IRQL-Überprüfung aktivieren, entfernt die Treiberüberprüfung jeglichen auslagerungsfähigen Kernelmoduscode und alle auslagerungsfähigen Kernelmodusdaten aus dem Systemarbeitssatz, sobald ein überwachter Gerätetreiber die IRQL anhebt. Dazu wird die interne Funktion MiTrimAllSystemPagableMemory verwendet. Wenn ein überwachter Gerätetreiber dann bei erhöhter IRQL auf auslagerungsfähigen Speicher zugreift, erkennt das System sofort die Verletzung. In dem resultierenden Systemabsturz wird der fehlerhafte Treiber angegeben. Ein weiterer häufiger Treiberabsturz aufgrund falscher IRQL-Verwendung tritt auf, wenn auf Synchronisierungsobjekte gewartet wird, die zu ausgelagerten Datenstrukturen gehören. Synchronisierungsobjekte sollten niemals ausgelagert werden, da der Dispatcher auch auf höheren IRQLs auf sie zugreifen muss, bei denen es sonst zu einem Absturz kommen würde. Die Treiberüberprüfung ermittelt, ob sich die Strukturen KTIMER, KMUTEX, KSPIN_LOCK, KEVENT, KSEMAPHORE, ERESOURCE und FAST_MUTEX im auslagerungsfähigen Speicher befinden. Simulierung geringer Ressourcen Bei der Option Simulierung geringer Ressourcen lässt die Treiberüberprüfung Speicherzuweisungen durch die überwachten Gerätetreiber nach dem Zufallsprinzip fehlschlagen. Früher wurden Gerätetreiber oft unter der Annahme geschrieben, dass der Kernelspeicher stets verfügbar ist. Wenn dieser Speicher ausging, war das für den Treiber auch kein Problem, da das System dann ohnehin abstürzen würde. Da eine Verknappung des Speichers jedoch auch vorübergehend auftreten kann und die heutigen Mobilgeräte nicht so leistungsfähig sind wie große Computer, müssen Gerätetreiber jedoch sauber mit Zuweisungsfehlern umgehen, die auf eine Erschöpfung des Kernelspeichers hindeuten. Zu den Treiberaufrufen, bei den zufällig ein Fehlschlag ausgelöst wird, gehören ExAllocatePool*, MmProbeAndLockPages, MmMapLockedPagesSpecifyCache, MmMapIoSpace, MmAllocateContiguousMemory, MmAllocatePagesForMdl, IoAllocateIrp, IoAllocateMdl, IoAllocateWorkItem, IoAllocateErrorLogEntry, IoSetCompletionRoutineEx sowie verschiedene Rtl-String-APIs, die Zuweisungen aus dem Pool vornehmen. Die Treiberüberprüfung lässt auch einige Zu- Treiberüberprüfung Kapitel 6 641
weisungen der GDI-Kernelfunktionen fehlschlagen. (Eine vollständige Liste finden Sie in der WDK-Dokumentation.) Darüber hinaus können Sie noch Folgendes angeben: QQ Die Wahrscheinlichkeit, mit der eine Zuweisung fehlschlägt trägt 6 %. QQ Die Anwendungen, die dieser Simulation unterzogen werden sind dies alle. QQ Die betroffenen Pooltags Der Standardwert beNach Voreinstellung Nach Voreinstellung sind dies alle. QQ Die Verzögerung, bevor die Simulation beginnt Vorgegeben sind sieben Minuten nach dem Systemstart. Das ist ausreichend Zeit, um die kritische Initialisierungsphase abzuschließen, in der knapper Speicher das Laden eines Gerätetreibers verhindern kann. Diese Einstellungen können Sie mit den Befehlszeilenoptionen von verifier.exe ändern. Nach Ablauf der Verzögerung beginnt die Treiberüberprüfung damit, Zuweisungsaufrufe der überwachten Gerätetreiber zufallsgesteuert fehlschlagen zu lassen. Wenn ein Treiber mit solchen Zuweisungsfehlern nicht korrekt umgehen kann, wird sich das sehr wahrscheinlich in einem Systemabsturz zeigen. Systematische Simulation geringer Ressourcen Ähnlich wie bei Simulierung geringer Ressourcen lässt die Treiberüberprüfung auch bei dieser Option bestimmte Aufrufe an den Kernel und an Ndis.sys (für Netzwerktreiber) fehlschlagen, allerdings auf systematische Weise. Dazu untersucht sie den Aufrufstack bei einem Fehler. Wenn der Treiber ihn korrekt gehandhabt hat, wird bei diesem Stack kein weiterer Fehlschlag mehr versucht. Dadurch können Treiberautoren die Probleme systematisch erkennen, ein gemeldetes Problem lösen und dann zum nächsten übergehen. Die Untersuchung von Aufrufstacks ist jedoch eine sehr aufwendige Operation, weshalb davon abzuraten ist, mehr als einen Treiber auf einmal mit dieser Einstellung zu überprüfen. Sonstige Prüfungen Bei einigen der Überprüfungen, die unter Sonstiges fallen, wird ermittelt, ob bestimmte Systemstrukturen im Pool freigegeben werden, obwohl sie noch aktiv sind. Beispielsweise kann die Treiberüberprüfung nach folgenden Situationen Ausschau halten: QQ Aktive Arbeitselemente im freigegebenen Speicher Ein Treiber ruft ExFreePool auf, um einen Poolblock freizugeben, in dem sich noch Arbeitselemente befinden, die mit I­ oQueueWorkItem in eine Warteschlange gestellt wurden. QQ Aktive Ressourcen im freigegebenen Speicher Ein Treiber ruft ExFreePool auf, ohne erst ExDeleteResource aufzurufen, um ein ERESOURCE-Objekt zu zerstören. QQ Aktive Look-Aside-Listen im freigegebenen Speicher Ein Treiber ruft ExFreePool auf, ohne erst ExDeleteNPagedLookasideList oder ExDeletePagedLookasideList aufzurufen, um die Look-Aside-Liste zu löschen. 642 Kapitel 6 Das E/A-System
Die Treiberüberprüfung führt darüber hinaus automatisch noch weitere Untersuchungen durch, die sich nicht einzeln aktivieren und deaktivieren lassen. Dazu gehören die Folgenden: QQ Aufruf von MmProbeAndLockPages oder MmProbeAndLockProcessPages für eine MDL mit falschen Flags. Beispielsweise ist es falsch, MmProbeAndLockPages für eine MDL aufzurufen, die durch den Aufruf von MmBuildMdlForNonPagedPools eingerichtet wurde. QQ Aufruf von MmMapLockedPages für eine MDL mit falschen Flags. Beispielsweise ist es falsch, MmMapLockedPages für eine MDL aufzurufen, die bereits auf eine Systemadresse abgebildet wurde. Ein weiteres Beispiel für falsches Treiberverhalten ist ein Aufruf von MmMapLockedPages für eine nicht verriegelte MDL. QQ Aufruf von MmUnlockPages oder MmUnmapLockedPages für eine Teil-MDL (erstellt durch IoBuildPartialMdl). QQ Aufruf von MmUnmapLockedPages für eine MDL, die nicht auf eine Systemadresse abgebildet wurde. QQ Zuweisen von Synchronisierungsobjekten wie Ereignissen oder Mutexen aus dem N­ onPagedPoolSession-Speicher. Die Treiberüberprüfung ist eine wertvolle Ergänzung der Verifizierungs- und Debuggingwerkzeuge für Treiberautoren. Viele Gerätetreiber, die mit der Treiberüberprüfung untersucht wurden, wiesen Bugs auf, die damit sichtbar gemacht werden konnten. Dadurch hat die Treiberüberprüfung zu einer allgemeinen Verbesserung des Kernelmoduscodes in Windows geführt. Der PnP-Manager Der PnP-Manager ist die Hauptkomponente für die Fähigkeit von Windows, geänderte Hardwarekonfigurationen zu erkennen und sich daran anzupassen. Ein Benutzer muss nichts von den Feinheiten der Hardware oder der manuellen Konfiguration verstehen, um Geräte zu in­ stallieren und zu entfernen. So ist der PnP-Manager beispielsweise auch dafür zuständig, dass ein laufender Windows-Laptop, der in einer Dockingstation platziert wird, automatisch andere Geräte an derselben Station erkennen und sie für den Benutzer verfügbar machen kann. Die Unterstützung für Plug & Play erfordert Kooperation auf der Ebene der Hardware, der Gerätetreiber und des Betriebssystems. Branchenstandards für die Auflistung und Identifizierung von Geräten an Bussen bilden die Grundlage für die Plug-&-Play-Unterstützung von Windows. Beispielsweise legt der USB-Standard fest, wie sich Geräte an einem USB-Bus selbst identifizieren. Auf dieser Grundlage bietet die Plug-&-Play-Unterstützung von Windows folgende Möglichkeiten: QQ Der PnP-Manager erkennt automatisch installierte Geräte. Dazu werden die beim Start an das System angeschlossenen Geräte aufgelistet und alle Ergänzungen und Wegnahmen von Geräten während der weiteren Ausführung des Systems erkannt. QQ Der PnP-Manager kümmert sich auch um die Zuweisung von Hardwareressourcen, indem er die entsprechenden Anforderungen der angeschlossenen Geräte erfasst (Interrupts, E/A-Speicher, E/A-Register oder busspezifische Ressourcen) und die Ressourcen durch den Der PnP-Manager Kapitel 6 643
Vorgang der Ressourcenvermittlung optimal zuweist, sodass jedes Gerät bekommt, was es für seinen Betrieb benötigt. Da Hardwaregeräte auch nach der Ressourcenzuweisung beim Start hinzugefügt werden können, muss der PnP-Manager auch in der Lage sein, Ressourcen neu zuzuweisen, um die Bedürfnisse der im laufenden Betrieb hinzugekommenen Geräte zu erfüllen. QQ Das Laden der geeigneten Treiber fällt ebenfalls in die Verantwortung des PnP-Managers. Er ermittelt, ob auf dem System ein Treiber installiert ist, der in der Lage ist, das betreffende Gerät zu verwalten. Wenn ja, weist er den E/A-Manager an, diesen Treiber zu laden. Ist kein geeigneter Treiber vorhanden, kommuniziert der PnP-Manager im Kernelmodus mit dem PnP-Manager im Benutzermodus, um das Gerät zu installieren, wobei er möglicherweise den Benutzer auffordert, einen geeigneten Treiber zu suchen. QQ Der PnP-Manager implementiert auch Mechanismen, mit denen Anwendungen und Treiber Änderungen an der Hardwarekonfiguration erkennen können. Da manche Anwendungen und Treiber für ihren Betrieb besondere Hardware benötigen, bietet Windows ihnen die Möglichkeit, Anforderungen über das Vorhandensein, das Hinzufügen und Entfernen von Geräten anzufordern. QQ Der PnP-Manager stellt Platz zur Speicherung des Gerätestatus zur Verfügung und beteiligt sich an der Einrichtung, Aktualisierung und Migration des Systems sowie an der Verwaltung von Offlineabbildern. QQ Der PnP-Manager unterstützt über das Netzwerk angeschlossene Geräte wie Projektoren und Drucker, indem er besonderen Bustreibern die Möglichkeit bietet, das Netzwerk als Bus aufzufassen und Geräteknoten für die dort laufenden Geräte zu erstellen. Der Grad der Unterstützung für Plug & Play Windows soll Plug & Play nach Möglichkeit vollständig unterstützen, was allerdings von den angeschlossenen Geräten und den installierten Treibern abhängt. Wenn ein einzelnes Gerät oder ein einziger Treiber Plug & Play nicht unterstützt, kann das den Grad der Plug-&-Play-Unterstützung des Systems einschränken. Es ist sogar möglich, dass ein Treiber, der Plug & Play nicht unterstützt, die Verwendung anderer Geräte im System verhindert. Tabelle 6–6 zeigt die verschiedenen möglichen Kombinationen von Geräten und Treibern, die keine Unterstützung für Plug & Play bieten. Gerät Plug-&-Play-Treiber Nicht-Plug-&-Play-Treiber Plug-&-Play-Gerät Vollständige Plug-&-Play-­ Unterstützung Kein Plug & Play Nicht-Plug-&-Play-Gerät Möglicherweise eingeschränkte Plug-&-Play-Unterstützung Kein Plug & Play Tabelle 6–6 Plug-&-Play-Fähigkeit von Geräten und Treibern Nicht Plug-&-Play-fähige Geräte, beispielsweise veraltete ISA-Soundkarten, bieten keine Unterstützung für die automatische Erkennung. Da das Betriebssystem nicht weiß, wo die Hardware physisch angeschlossen ist, sind bestimmte Operationen – z. B. das Entnehmen eines 644 Kapitel 6 Das E/A-System
Laptops aus einer Dockingstation, Energiesparmodus und Ruhezustand – nicht zulässig. Wird für das Gerät jedoch manuell ein Plug-&-Play-Treiber installiert, kann dieser zumindest eine vom PnP-Manager gelenkte Ressourcenzuweisung für das Gerät implementieren. Zu den nicht Plug-&-Play-fähigen Treibern gehören veraltete Treiber wie diejenigen, die auf Windows NT 4 liefen. Sie funktionieren unter Umständen zwar auch auf neueren Versionen von Windows, doch der PnP-Manager kann die Ressourcen, die den entsprechenden Geräten zugewiesen wurden, nicht neu einstellen, wenn eine Neuzuweisung von Ressourcen durch das dynamische Hinzufügen weiterer Geräte erforderlich ist. Betrachten Sie als Beispiel ein Gerät, das die E/A-Speicherbereiche A und B nutzen kann. Während des Starts weist ihm der PnP-­ Manager Bereich A zu. Wird dem System später ein Gerät hinzugefügt, das auf die Verwendung von A angewiesen ist, kann der PnP-Managern den Treiber des ersten Geräts nicht anweisen, sich für Bereich B umzukonfigurieren. Das hindert das zweite Gerät daran, die erforderlichen Ressourcen zu erwerben, weshalb es vom System nicht genutzt werden kann. Veraltete Treiber beeinträchtigen auch die Möglichkeiten des Computers, in den Energiesparmodus oder den Ruhezustand überzugehen (siehe den Abschnitt über die Energieverwaltung weiter hinten in diesem Kapitel). Geräteauflistung Eine Geräteauflistung erfolgt, wenn das System hochgefahren wird, aus dem Ruhezustand erwacht oder ausdrücklich dazu aufgefordert wird (etwa durch einen Klick auf Nach geänderter Hardware suchen im Dialogfeld Geräte-Manager). Der PnP-Manager erstellt einen Gerätebaum (den wir in Kürze beschreiben werden) und vergleicht ihn ggf. mit dem gespeicherten Baum einer vorherigen Auflistung. Beim Hochfahren und beim Aufwachen aus dem Ruhezustand ist der gespeicherte Gerätebaum leer. Neu ermittelte und entfernte Geräte benötigen eine besondere Behandlung, z. B. das Laden der passenden Treiber (für neu hinzugekommene Geräte) bzw. die Benachrichtigung der Treiber darüber, dass das Gerät entfernt wurde. Der PnP-Manager beginnt die Geräteauflistung mit einem virtuellen Bustreiber, der als Stammgerät bezeichnet wird. Er steht für das gesamte Computersystem und fungiert als Bustreiber für nicht Plug-&-Play-fähige Treiber und die HAL. Die HAL wiederum dient als Bustreiber für die Auflistung der direkt an die Hauptplatine angeschlossenen Geräte sowie von Systemkomponenten wie Akkus. Um den primären Bus (in den meisten Fällen ist das der PCI-Bus) und Geräte wie Akkus und Lüfter zu erkennen, führt die HAL jedoch nicht selbst eine Auflistung durch, sondern verlässt sich auf die Hardwarebeschreibung, die der Setupprozess in der Registrierung gespeichert hat. Der primäre Bustreiber listet die Geräte an seinem Bus auf. Möglicherweise findet er dabei weitere Busse, für die der PnP-Manager dann Treiber initialisiert. Diese Treiber wiederum können weitere Geräte entdecken, darunter auch weitere untergeordnete Busse. Dieser rekursive Vorgang der Auflistung, des Ladens von Treibern (falls der entsprechende Treiber nicht bereits geladen ist) und der weiteren Auflistung wird fortgesetzt, bis alle Geräte am System ermittelt und konfiguriert worden sind. Wenn die Bustreiber dem PnP-Manager erkannte Geräte melden, erstellt dieser einen internen Gerätebaum, der die Beziehungen zwischen den Geräten darstellt. Die Knoten in diesem Baum werden als Geräteknoten oder auch kurz als Devnodes bezeichnet. Ein solcher Der PnP-Manager Kapitel 6 645
Knoten enthält Informationen über die Geräteobjekte, die das Gerät darstellen, sowie weitere Plug-&-Play-Informationen, die der PnP-Manager in dem Knoten gespeichert hat. Abb. 6–32 zeigt ein vereinfachtes Beispiel eines solchen Baums. Hier fungiert ein PCI-Bus als Primärbus des Systems und verfügt über einen USB-, einen ISA- und einen SCSI-Bus. Kamera Maus Externes Modem Serieller Port USB-Hub USBController Tastatur PCI-ISABrücke ACPI-Lüfter PCI-Bus Festplatte SCSIAdapter ACPI-Akku ACPI Stamm­ gerät Abbildung 6–32 Beispiel für einen Gerätebaum Der Geräte-Manager, der unter Start > Programme > Verwaltung > Computerverwaltung zur Verfügung steht (sowie unter dem Link Geräte-Manager im Dienstprogramm System der Systemsteuerung), zeigt in seiner Standardansicht eine einfache Liste der Geräte, die auf dem System vorhanden sind. Im Menü Ansicht des Geräte-Managers können Sie jedoch auch die Option Geräte nach Verbindung auswählen, sodass die Geräte wie im Gerätebaum angezeigt werden (siehe Abb. 6–33). 646 Kapitel 6 Das E/A-System
Abbildung 6–33 Der Geräte-Manager mit der Ansicht des Gerätebaums Experiment: Den Gerätebaum ausgeben Mit dem Kerneldebuggerbefehl !devnode können Sie sich eine ausführlichere Ansicht des Gerätebaums anzeigen lassen, als sie der Geräte-Manager bietet. Wenn Sie die Optionen 0 1 angeben, werden die internen Geräteknotenstrukturen des Gerätebaums ausgegeben, wobei die Einrückungen die hierarchischen Beziehungen zeigen: lkd> !devnode 0 1 Dumping IopRootDeviceNode (= 0x85161a98) DevNode 0x85161a98 for PDO 0x84d10390 InstancePath is "HTREE\ROOT\0" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) DevNode 0x8515bea8 for PDO 0x8515b030 DevNode 0x8515c698 for PDO 0x8515c820 InstancePath is "Root\ACPI_HAL\0000" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) DevNode 0x84d1c5b0 for PDO 0x84d1c738 → Der PnP-Manager Kapitel 6 647
InstancePath is "ACPI_HAL\PNP0C08\0" ServiceName is "ACPI" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) DevNode 0x85ebf1b0 for PDO 0x85ec0210 InstancePath is "ACPI\GenuineIntel_-_x86_Family_6_Model_15\_0" ServiceName is "intelppm" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) DevNode 0x85ed6970 for PDO 0x8515e618 InstancePath is "ACPI\GenuineIntel_-_x86_Family_6_Model_15\_1" ServiceName is "intelppm" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) DevNode 0x85ed75c8 for PDO 0x85ed79e8 InstancePath is "ACPI\ThermalZone\THM_" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) DevNode 0x85ed6cd8 for PDO 0x85ed6858 InstancePath is "ACPI\pnp0c14\0" ServiceName is "WmiAcpi" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) DevNode 0x85ed7008 for PDO 0x85ed6730 InstancePath is "ACPI\ACPI0003\2&daba3ff&2" ServiceName is "CmBatt" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) DevNode 0x85ed7e60 for PDO 0x84d2e030 InstancePath is "ACPI\PNP0C0A\1" ServiceName is "CmBatt" ... Bei den einzelnen Geräteknoten wird u. a. der Instanzpfad (InstancePath) angezeigt. Das ist der Name des Auflistungsschlüssels für das Gerät unter HKLM\SYSTEM\CurrentControlSet\ Enum. Ebenfalls angegeben ist der Dienstname (ServiceName), der dem Treiberschlüssel für das Gerät unter HKLM\SYSTEM\CurrentControlSet\Services entspricht. Um die den einzelnen Geräteknoten zugewiesenen Ressourcen wie Interrupts, Ports und Arbeitsspeicher einzusehen, geben Sie beim Befehl !devnode die Optionen 0 3 an. 648 Kapitel 6 Das E/A-System
Gerätestacks Wenn der PnP-Manager Geräteknoten erstellt, legt er auch die Treiber- und Geräteobjekte an, um die Verknüpfung zwischen den Geräten, die den Knoten ausmachen, zu verwalten und logisch darzustellen. Diese Verknüpfung wird als Gerätestack bezeichnet (und wurde im Abschnitt »IRP-Fluss« weiter vorn in diesem Kapitel schon kurz erwähnt). Diesen Gerätestack können Sie sich als geordnete Liste von Geräteobjekt-Treiber-Paaren vorstellen. Ein Gerätestack wird immer von unten nach oben aufgebaut. Abb. 6–34 zeigt ein Beispiel für einen Geräteknoten (identisch mit Abb. 6–6) mit sieben Geräteobjekten (die alle dasselbe physische Gerät verwalten). Jeder Geräteknoten enthält mindestens zwei Geräte (PDO und FDO), kann aber noch mehr Geräteobjekte einschließen. Ein Gerätestack besteht aus folgenden Elementen: QQ Ein physisches Geräteobjekt (Physical Device Object, PDO). Wenn ein Bustreiber bei der Auflistung das Vorhandensein eines Geräts an seinem Bus meldet, weist der PnP-Manager ihn an, dieses Objekt zu erstellen. Das PDO stellt die physische Schnittstelle zu dem Gerät dar und befindet sich immer am Boden des Gerätestacks. QQ Ein oder mehrere optionale Filtergeräteobjekte (FiDOs) zwischen dem PDO und dem funktionalen Geräteobjekt (FDO; siehe den nächsten Punkt). Sie werden als untere Filter bezeichnet (in Bezug auf das FDO) und können dazu verwendet werden, IRPs abzufangen, die vom FDO zum Bustreiber laufen (und von Interesse für die Busfilter sind). QQ Genau ein funktionales Geräteobjekt (FDO). Erstellt wird es von dem Funktionstreiber, den der PnP-Manager zur Verwaltung des erkannten Geräts lädt. Ein FDO stellt die logische Schnittstelle zu dem Gerät dar und hat die ausführlichsten Kenntnisse über den Funk­ tionsumfang des Geräts. Ein Funktionstreiber kann auch als Bustreiber fungieren, wenn an das Gerät, das durch das FDO dargestellt wird, andere Geräte angeschlossen sind. Häufig erstellt der Funktionstreiber eine Schnittstelle (im Grunde genommen nur ein Name) zu dem PDO für das FDO, damit Anwendungen und andere Treiber das Gerät öffnen und damit arbeiten können. Manche Funktionstreiber bestehen aus einem Klassen- oder Porttreiber sowie einem Miniporttreiber, die zusammenarbeiten, um die E/A für das FDO zu verwalten. QQ Ein oder mehrere optionale FiDOs oberhalb des FDOs (obere Filter). Sie sind die ersten, die mit dem IRP-Header für das FDO arbeiten dürfen. In Abb. 6–34 haben die Geräteobjekte verschiedene Namen, um sie einfacher beschreiben zu können. Allerdings sind alle Instanzen von DEVICE_OBJECT-Strukturen. Der PnP-Manager Kapitel 6 649
Oberer Filtertreiber 3 Oberer Filtertreiber 2 Oberer Filtertreiber 1 Funktionstreiber Unterer Filtertreiber 2 Unterer Filtertreiber 1 Bustreiber Abbildung 6–34 Geräteknoten (Gerätestack) Gerätestacks werden von unten nach oben aufgebaut und nutzen die Schichtungsmöglichkeiten des E/A-Managers, sodass IRPs von der Spitze des Stacks zum Boden fließen können. Wie im Abschnitt »IRP-Fluss« weiter vorn in diesem Kapitel bereits erklärt, kann ein IRP aber von jeder Schicht des Gerätestacks abgeschlossen werden. Treiber in den Gerätestack laden Wie findet der PnP-Manager die richtigen Treiber, um den Gerätestack aufzubauen? Die entsprechenden Informationen sind in drei Schlüsseln der Registrierung (und ihren Unterschlüsseln) verstreut, wie Sie in Tabelle 6–7 sehen. (CCS steht hier für CurrentControlSet.) Registrierungsschlüssel Kurzname Beschreibung HKLM\System\CCS\Enum Hardwareschlüssel Einstellungen für bekannte Hardwaregeräte HKLM\System\CCS\Control\Class Klassenschlüssel Einstellungen für Gerätetypen HKLM\System\CCS\Services Softwareschlüssel Einstellungen für Treiber Tabelle 6–7 Registrierungsschlüssel zum Laden von Plug-&-Play-Treibern Wenn ein Bustreiber eine Geräteauflistung durchführt und ein neues Gerät entdeckt, erstellt er als Erstes ein PDO, das für das Vorhandensein dieses physischen Geräts steht. Danach informiert er den PnP-Manager, indem er die im WDK dokumentierte Funktion IoInvalidate­ DeviceRelations mit dem Auflistungswert BusRelations und dem PDO aufruft. Das sagt dem PnP-Manager, dass eine Änderung an dem Bus erkannt wurde. Der PnP-Manager reagiert darauf, indem er den Bustreiber (durch ein IRP) nach der Gerätekennung fragt. Diese Kennungen sind busspezifisch. Beispielsweise bestehen die Kennungen von USB-­ Geräten aus der ID des Hardwareherstellers (Vendor ID, VID) und einer Produkt-ID (PID), die dieser Hersteller dem Gerät zugewiesen hat. Bei einem PCI-Gerät wird neben einer ähnlichen Hersteller-ID auch eine Geräte-ID benötigt, um das Gerät in der Produktpalette des Herstellers eindeutig zu bezeichnen. (Dazu kommen noch einige optionale Komponenten. Mehr Infor- 650 Kapitel 6 Das E/A-System
mationen über das Format von Gerätekennungen finden Sie im WDK.) Zusammengenommen ergibt sich das, was bei Plug & Play als Gerätekennung oder Geräte-ID bezeichnet wird. Der PnP-Manager fragt den Bustreiber auch nach einer Instanz-ID, um zwischen mehreren Exemplaren der gleichen Hardware unterscheiden zu können. Die Instanz-ID kann dabei entweder eine relative Platzierung am Bus (z. B. den USB-Anschluss) angeben oder ein global eindeutiger Bezeichner sein (z. B. die Seriennummer). Geräte- und Instanz-ID werden zu einer Geräteinstanz-ID (Device Instance ID, DIID) kombiniert. Darüber kann der PnP-Manager den Schlüssel für das Gerät im Hardwareschlüssel (siehe Tabelle 6–7) finden. Die Unterschlüssel haben die Form <Bustreiber>\<Geräte-ID>\­ <Instanz-ID>. Abb. 6–35 zeigt ein Beispiel für einen Auflistungs-Unterschlüssel für eine Intel-Grafikkarte. Der Schlüssel des Geräts enthält neben beschreibenden Daten die Werte Service und ClassGUID (die bei der Installation aus der INF-Datei des Treibers bezogen werden), mit denen der PnP-Manager die Treiber für das Gerät wie folgt finden kann: QQ Er schlägt den Wert von Service im Softwareschlüssel nach, wo der Pfad zu dem Treiber (SYS-Datei) im Wert ImagePath gespeichert ist. Abb. 6–36 zeigt den Softwareschlüssel igfx, der im Eintrag Service von Abb. 6–35 angegeben war und in dem der Pfad zu dem Intel-Grafiktreiber verzeichnet ist. Der PnP-Manager lädt den Treiber (falls das nicht bereits geschehen ist) und ruft dessen Routine zum Hinzufügen von Geräten auf. Der Treiber erstellt dann das FDO. QQ Der unter Umständen vorhandene Wert LowerFilters enthält mehrere Stringlisten von Treibern, die als untere Filter geladen werden müssen. Auch sie können über den Softwareschlüssel gefunden werden. Der PnP-Manager lädt diese Filtertreiber vor dem Treiber, der im Service-Wert angegeben ist. Abbildung 6–35 Beispiel für einen Hardwareschlüssel Der PnP-Manager Kapitel 6 651
Abbildung 6–36 Beispiel für einen Softwareschlüssel QQ Es kann auch der Wert UpperFilters mit einer Liste von Treibernamen vorhanden sein (wobei diese Treiber ebenso wie bei LowerFilters im Softwareschlüssel verzeichnet sind). Der PnP-Manager lädt diese Treiber nach demjenigen, der im Wert Service genannt wurde. QQ Der Wert ClassGUID steht für einen allgemeinen Gerätetyp (Anzeige, Tastatur, Festplatte usw.) und zeigt auf einen Unterschlüssel des Klassenschlüssels (siehe Tabelle 6–7). Dieser Schlüssel stellt die Einstellungen dar, die für alle Treiber dieses Gerätetyps gelten. Ist hier beispielsweise der Wert LowerFilters oder UpperFilters vorhanden, so wird er genauso behandelt wie diejenigen im Hardwareschlüssel für ein bestimmtes Gerät. Dadurch ist es beispielsweise möglich, einen oberen Filter für Tastaturgeräte unabhängig von einem spezifischen Gerät oder Hersteller zu laden. Abb. 6–37 zeigt den Klassenschlüssel für Tastaturen. Es gibt zwar einen für Menschen verständlichen Anzeigenamen (Keyboard), aber worauf es wirklich ankommt, ist der GUID. (Zu welcher Klasse ein Gerät gehört, wird in der INF-Datei angegeben.) In diesem Beispiel ist der Wert UpperFilters vorhanden und gibt die vom System bereitgestellten Tastaturklassentreiber an, die für jeden Tastaturgeräteknoten geladen werden. (Darüber hinaus gibt der Wert IconPath das Symbol an, das in der Benutzeroberfläche des Geräte-Managers für Tastaturen verwendet werden soll.) Abbildung 6–37 Der Klassenschlüssel für Tastaturen Die Treiber für einen Geräteknoten werden also in folgender Reihenfolge geladen: 1. Der Bustreiber wird geladen und erstellt das PDO. 2. Jegliche im Hardwareschlüssel aufgeführten unteren Filter werden in der angegebenen Reihenfolge (Multistring) geladen, wobei ihre Filtergerätobjekte erstellt werden (die FiDOs aus Abb. 6–34). 3. Jegliche im zugehörigen Klassenschlüssel aufgeführten unteren Filter werden in der angegebenen Reihenfolge geladen, wobei ihre FiDOs erstellt werden. 4. Der im Wert Service genannte Treiber wird geladen und das FDO wird erstellt. 5. Jegliche im Hardwareschlüssel aufgeführten oberen Filter werden in der angegebenen Reihenfolge geladen, wobei ihre FiDOs erstellt werden. 652 Kapitel 6 Das E/A-System
6. Jegliche im zugehörigen Klassenschlüssel aufgeführten oberen Filter werden in der angegebenen Reihenfolge geladen, wobei ihre FiDOs erstellt werden. Um mit Multifunktionsgeräten umgehen zu können (z. B. Multifunktionsdrucker oder Smartphones mit eingebauter Kamera und Möglichkeiten zur Musikwiedergabe), ist es in Windows möglich, mit einem Geräteknoten auch eine Container-ID zu verknüpfen. Dieser GUID ist für eine einzelne Instanz eines physischen Geräts eindeutig, wird aber von allen seinen Geräteknoten für die einzelnen Funktionen gemeinsam genutzt (siehe Abb. 6–38). Windows-PC Container für Multi­ funktions­gerät Plug-&-Play-Geräteknoten Multifunktionsgerät • Drucker • Scanner • Fax Abbildung 6–38 Multifunktionsdrucker mit einer eindeutigen ID aus der Sicht des PnP-Managers Die Container-ID wird ähnlich wie die Instanz-ID von dem Bustreiber der zugehörigen Hardware gemeldet. Bei der Auflistung des Geräts teilen sich alle mit dem PDO verknüpften Geräteknoten diese Container-ID. Da Windows bereits im Lieferzustand mehrere Busse unterstützt – z. B. PnP-X, Bluetooth und USB –, können die meisten Gerätetreiber einfach die busspezifische ID zurückgeben, aus der Windows dann die entsprechende Container-ID generiert. Bei anderen Arten von Geräten und Bussen können die Treiber ihre eigene eindeutige ID mithilfe von Software erstellen. Bei Gerätetreibern, die keine Container-IDs unterstützen, kann Windows gezielt raten, indem es die Topologie für den Bus durch Mechanismen wie ACPI abfragt (sofern vorhanden). Wenn Windows weiß, ob ein bestimmtes Gerät einem anderen untergeordnet ist, ob es sich um ein Wechselgerät, ein im laufenden Betrieb austauschbares Gerät oder ein für den Benutzer zugängliches Gerät (im Gegensatz zu einer internen Komponente auf dem Motherboard) handelt, kann es den Geräteknoten Container-IDs zuweisen, die die Multifunktionsgeräte korrekt widerspiegeln. Der Vorteil, den die Gruppierung von Geräten nach Container-IDs für den Endbenutzer hat, wird im Dialogfeld Geräte und Drucker sichtbar, denn dadurch können die Scanner-, die Drucker- und die Fax-Komponente eines Multifunktionsdruckers durch ein einziges grafisches Element dargestellt werden anstatt als drei getrennte Geräte. In Abb. 6–39 ist der Multifunk­ tionsdrucker HP 6860 als ein einziges Gerät angegeben. Der PnP-Manager Kapitel 6 653
Abbildung 6–39 Das Applet Geräte und Drucker in der Systemsteuerung Experiment: Ausführliche Informationen über einen Geräteknoten im Geräte-Manager anzeigen Auf der Registerkarte Details des Geräte-Managers werden ausführliche Informationen über einen Geräteknoten angezeigt. Hier können Sie verschiedene Felder einsehen, darunter die Geräteinstanz-ID, die Hardware-ID, den Dienstnamen, die Filter und Energiedaten des Geräteknotens. Der folgende Screenshot stellt das ausgeklappte Dropdownmenü der Registerkarte Details dar und zeigt, auf welche Arten von Informationen Sie hier zugreifen können: 654 Kapitel 6 Das E/A-System
Treiberunterstützung für Plug & Play Zur Unterstützung von Plug & Play muss ein Treiber eine PnP-Dispatchroutine (IRP_MJ_PNP), eine Dispatchroutine für die Energieverwaltung (IRP_MJ_POWER; siehe den Abschnitt »Energieverwaltung« weiter hinten in diesem Kapitel) und eine Routine zum Hinzufügen von Geräten implementieren. Allerdings müssen Bustreiber andere Arten von Plug-&-Play-Anforderungen unterstützen als Funktions- und Filtertreiber. Beispielsweise fragt der PnP-Manager die Bustreiber während der Geräteauflistung zum Systemstart mithilfe von PRP-IRPs nach Beschreibungen der Geräte an ihren jeweiligen Bussen. Funktions- und Filtertreiber bereiten die Verwaltung ihrer Geräte in den Routinen zum Hinzufügen von Geräten vor. Sie kommunizieren jedoch nicht mit der Gerätehardware, sondern warten darauf, dass der PnP-Manager einen Befehl zum Starten des Geräts (PnP-IRP-Nebenfunktionscode IRP_MIN_START_DEVICE) an ihre PnP-Dispatchroutine sendet. Dieser Startbefehl enthält auch die Ressourcenzuweisung für das Gerät, die der PnP-Manager zuvor bei der Ressourcenvermittlung ermittelt hat. Wenn ein Treiber den Startbefehl erhält, kann er sein Gerät so einrichten, dass es die angegebenen Ressourcen nutzt. Versucht eine Anwendung, ein Gerät zu öffnen, dessen Startvorgang noch nicht abgeschlossen ist, erhält sie die Fehlermeldung, dass das Gerät nicht existiert. Nach dem Start eines Geräts kann der PnP-Manager dem Treiber weitere Plug-&-PlayBefehle senden, unter anderem auch, um das Gerät wieder von dem System zu entfernen oder um die Ressourcen neu zuzuweisen. Wenn der Benutzer beispielsweise die Auswerfen-Funktion aufruft, die über das Symbol mit dem USB-Stecker in der Taskleiste erreichbar ist, um Windows dazu aufzufordern, einen USB-Stick auszuwerfen (siehe Abb. 6–40), sendet der PnP-Manager an alle Anwendungen, die sich für Plug-&-Play-Benachrichtigungen für dieses Gerät registriert haben, eine QUERY_REMOVE-Benachrichtigung. Anwendungen registrieren sich gewöhnlich für Benachrichtigungen, die ihre Handles betreffen. Beim Eingang von QUERY_REMOVE schließen sie diese Handles. Wenn keine Anwendung ihr Veto gegen diese Anforderung einlegt, sendet der PnP-Manager eine entsprechende Benachrichtigung an den Treiber, dem das auszuwerfende Gerät gehört (IRP_MIN_QUERY_REMOVE_DEVICE). Der Treiber kann jetzt das Entfernen des Geräts verweigern oder dafür sorgen, dass alle ausstehenden E/A-Operationen im Zusammenhang mit dem Gerät abgeschlossen und jegliche neuen E/A-Anforderungen abgelehnt werden. Wenn der Treiber dem Entfernen zustimmt und es keine offenen Handles zu dem Gerät mehr gibt, sendet der PnP-Manager als Nächstes einen Entfernbefehl an den Treiber (IRP_MN_­REMOVE_ DEVICE), um diesen aufzufordern, nicht mehr auf das Gerät zuzugreifen und jegliche von ihm für das Gerät zugewiesenen Ressourcen freizugeben. Abbildung 6–40 Auswerfen eines Geräts Wenn der PnP-Manager die Ressourcen eines Geräts neu zuweisen muss, sendet er dem Treiber eine Anhalteanfrage (IRP_MN_QUERY_STOP_DEVICE), um ihn aufzufordern, die Aktivitäten auf dem Der PnP-Manager Kapitel 6 655
Gerät vorübergehend einzustellen. Der Treiber kann dieser Aufforderung zustimmen (wenn das nicht zu Datenverlusten oder -beschädigungen führt) oder sie ablehnen. Wenn der Treiber einverstanden ist, schließt er wie bei der Anfrage zum Entfernen alle ausstehenden E/A-Operationen ab und löst keine weiteren E/A-Anforderungen aus, die nicht abgebrochen und später neu gestartet werden können. Neue E/A-Anforderungen stellt der Treiber gewöhnlich in eine Warteschlange, sodass die Anwendungen, die zurzeit auf das Gerät zugreifen, von der Neuordnung der Ressourcen nichts mitbekommen. Der PnP-Manager sendet dem Treiber einen Stoppbefehl (IRP_MN_STOP_DEVICE). Anschließend kann er den Treiber unmittelbar anweisen, dem Gerät andere Ressourcen zuzuordnen, um ihm dann wieder einen Startbefehl für das Gerät zu senden. Im Grunde genommen bilden die Plug-&-Play-Befehle die Zustandsübergänge für ein Gerät in einem Zustandsautomaten. (Das Zustandsdiagramm aus Abb. 6–41 stellt den Zustandsautomaten dar, wie er durch die Funktionstreiber implementiert wird. Die Bustreiber realisieren einen weit vielschichtigeren Zustandsautomaten.) Jeder Übergang in Abb. 6–41 ist durch den IRP-Befehlsnamen ohne das Präfix IRP_MN_ gekennzeichnet. Ein Status, den wir noch nicht erläutert haben, ist derjenige, der durch den Befehl IRP_MN_SURPRISE_REMOVAL des PnP-Managers zustande kommt. Dies geschieht, wenn ein Benutzer ein Gerät ohne Warnung entfernt, wenn er eine PCMCIA-Karte unter Umgehung der Auswerfen-Funktion entnimmt oder wenn das Gerät fehlschlägt. Dieser Befehl weist den Treiber an, sofort jegliche Interaktion mit dem Gerät einzustellen, da das Gerät nicht mehr mit dem System verbunden ist, und alle ausstehenden E/A-Anforderungen abzubrechen. Gerät erkannt, Treiber geladen, DriverEntry aufgerufen, AddDevice aufgerufen Nicht gestartet Entfernen ausstehend Entfernt Gestartet Anhalten ausstehend Angehalten Überraschendes Entfernen Abbildung 6–41 Plug-&-Play-Statusübergänge Installation von Plug-&-Play-Treibern Wenn der PnP-Manager auf ein Gerät stößt, für das kein Treiber installiert ist, überlässt er den erforderlichen Installationsvorgang dem PnP-Manager des Benutzermodus. Wird das Gerät beim Systemstart erkannt, wird dafür bereits ein Geräteknoten angelegt, der Ladevorgang 656 Kapitel 6 Das E/A-System
aber bis zum Start des Benutzermodus-PnP-Managers aufgeschoben. (Der Dienst für den PnP-­ Manager im Benutzermodus ist in Umpnpmgr.dll implementiert, die wiederum in einer Standardinstanz von Svchost.exe läuft.) Die Komponenten, die an der Treiberinstallation beteiligt sind, sehen Sie in Abb. 6–42. Die dunkel getönten Objekte sind Komponenten, die im Allgemeinen vom System bereitgestellt werden, während es sich bei den helleren um diejenigen handelt, die in den Installationsdateien des Treibers enthalten sind. Als Erstes informiert der Bustreiber den PNP-Treiber anhand der Geräte-ID über ein Gerät, das er auflistet (1). Der PnP-Manager sucht in der Registrierung nach einem zugehörigen Funktionstreiber. Wenn er keinen findet, informiert er den PnP-Manager des Benutzermodus über das neue Gerät, wobei er ebenfalls die Geräte-ID angibt (2). Der PnP-Manager des Benutzermodus versucht nun zuerst, eine automatische Installation ohne Eingreifen des Benutzers durchzuführen. Wenn es für die Installation erforderlich ist, Dialogfelder anzuzeigen und den Benutzer einzubeziehen, und der zurzeit angemeldete Benutzer über Administratorrechte verfügt, startet der PnP-Manager des Benutzermodus die Anwendung Rundll32.exe (die auch die klassischen .cpl-Systemsteuerungsprogramme beherbergt), um den Hardwareinstallations-Assistenten %SystemRoot%\System32\Newdev.dll auszuführen (3). Hat der angemeldete Benutzer dagegen keine Administratorrechte (oder ist kein Benutzer angemeldet) und erfordert die Installation des Geräts ein Eingreifen des Benutzers, so schiebt der PnP-Manager des Benutzermodus die Installation auf, bis ein Benutzer mit entsprechenden Rechten angemeldet ist. Der Hardwareinstallations-Assistent verwendet die API-Funktionen DSetupapi.dll und CfgMg32.dll (Konfigurations-Manager), um die INF-Dateien für Treiber zu finden, die mit dem erkannten Gerät kompatibel sind. Bei diesem Vorgang kann es erforderlich sein, dass der Benutzer ein Installationsmedium mit den INF-Dateien des Herstellers einlegt. Es kann auch sein, dass der Assistent eine geeignete INF-Datei im Treiberspeicher findet (­%SystemRoot%\System32\DriverStore), der mit Windows ausgelieferte und über Windows Update heruntergeladene Treiber enthält. Die Installation erfolgt in zwei Schritten. Im ersten Schritt importiert der Drittanbieter-Treiberentwickler das Treiberpaket in den Treiberspeicher, und im zweiten führt das System die eigentliche Installation mithilfe des Prozesses %SystemRoot%\System32\Drvinst.exe durch. Hardware­installations- Setup- und CfgMgrAssistent APIs Klasseninstaller und Co-Installer PnP-Manager im Benutzermodus INF-Dateien, CAT-Dateien, Regis­trierung Benutzermodus Kernelmodus Filtertreiber PnP-Manager Funktionstreiber Filtertreiber Bustreiber Abbildung 6–42 An der Installation beteiligte Komponenten Der PnP-Manager Kapitel 6 657
Um Treiber für das neue Gerät zu finden, ruft der Installationsprozess eine Liste von Hardware-IDs (siehe die Erklärung weiter vorn) und kompatibler IDs vom Bustreiber ab. Kompatible IDs sind allgemeiner gehalten. Beispielsweise kann eine USB-Maus von einem bestimmten Hersteller über eine besondere Taste für eine einzigartige Funktion verfügen. Ist der spezifische Treiber nicht vorhanden, kann mit der kompatiblen ID allgemein für eine Maus jedoch auch ein generischer, im Lieferumfang von Windows enthaltener Treiber verwendet werden, um wenigstens die grundlegende, übliche Funktionalität einer Maus bereitzustellen. Die IDs beschreiben die verschiedenen Möglichkeiten zur Bezeichnung der Hardware in einer Treiberinstallationsdatei (INF). Die Listen sind so geordnet, dass die spezifischste Beschreibung der Hardware an erster Stelle steht. Werden in mehreren INFs Übereinstimmungen gefunden, wird wie folgt vorgegangen: QQ Spezifischere Übereinstimmungen werden gegenüber allgemeineren bevorzugt. QQ Digital signierte INFs werden gegenüber nicht signierten bevorzugt. QQ Jüngere signierte INFs werden gegenüber älteren signierten bevorzugt. Bei einer Übereinstimmung aufgrund einer kompatiblen ID kann der Hardware­ installations-Assistent für den Fall, dass zusammen mit der Hardware ein neu­ erer Treiber geliefert wurde, nach einem Installationsmedium fragen. Die INF-Datei verweist auf die Dateien des Funktionstreibers und enthält Anweisungen, um den Auflistungs- und den Klassenschlüssel in der Registrierung auszufüllen und die erforderlichen Dateien zu kopieren. Des Weiteren kann sie den Hardwareinstallations-Assistenten anweisen, die DLLs für Klassen- oder Co-Installer des Geräts zu starten (4), um klassen- oder gerätespezifische Installationsschritte auszuführen, z. B. die Anzeige von Konfigurationsdialogfeldern, in denen der Benutzer Einstellungen vornehmen kann. Beim Laden der Treiber, die den Geräteknoten bilden, wird schließlich der Geräte-/Treiberstack erstellt (5). Experiment: Die INF-Datei eines Treibers untersuchen Wenn ein Treiber oder eine andere Software mit einer INF-Datei installiert wird, kopiert das System diese Datei in das Verzeichnis %SystemRoot\Inf. Eine Datei ist dort immer vorhanden, nämlich Keyboard.inf für den Tastaturklassentreiber. Ihren Inhalt können Sie sich ansehen, indem Sie sie im Editor öffnen (wobei alles, was hinter einem Semikolon steht, ein Kommentar ist): ; ; KEYBOARD.INF -- This file contains descriptions of Keyboard class devices ; ; ; Copyright (c) Microsoft Corporation. All rights reserved. → 658 Kapitel 6 Das E/A-System
[Version] Signature ="$Windows NT$" Class =Keyboard ClassGUID ={4D36E96B-E325-11CE-BFC1-08002BE10318} Provider =%MSFT% DriverVer =06/21/2006,10.0.10586.0 [SourceDisksNames] 3426=windows cd [SourceDisksFiles] i8042prt.sys = 3426 kbdclass.sys = 3426 kbdhid.sys = 3426 ... Eine INF-Datei hat das klassische INI-Format, bei denen die Abschnittsüberschriften in eckigen Klammern stehen und darunter jeweils die Schlüssel-Wert-Paare, in denen Schlüssel und Wert jeweils durch ein Gleichheitszeichen verbunden sind. Eine INF-Datei wird nicht nacheinander von Anfang bis Ende abgearbeitet, sondern eher wie ein Baum: Einzelne Werte zeigen auf Abschnitte mit dem Wertnamen, an dem die Ausführung fortgesetzt werden soll. (Einzelheiten dazu entnehmen Sie bitte dem WDK.) Wenn Sie die Datei nach .sys durchsuchen, finden Sie die Abschnitte, die den PnP-Manager des Benutzermodus anweisen, die Klassentreiber i8042prt.sys und kbdclass.sys zu installieren: ... [i8042prt_CopyFiles] i8042prt.sys,,,0x100 [KbdClass.CopyFiles] kbdclass.sys,,,0x100 ... Vor der Installation eines Treibers prüft der PnP-Manager des Benutzermodus die Treibersignierrichtlinie des Systems. Wenn sie vorsieht, dass das System die Installation nicht signierter Treiber blockieren oder dabei zumindest eine Warnung ausgeben soll, durchsucht der PnP-­ Manager des Benutzermodus die INF-Datei des Treibers nach einem Eintrag mit einem Verweis auf einen Katalog (eine Datei mit der Erweiterung .cat), der die digitale Signatur des Treibers enthält. Die Windows Hardware Quality Labs (WHQL) von Microsoft prüfen die mit Windows mitgelieferten und die von Hardwareherstellern eingereichten Treiber. Wenn ein Treiber die WHQL-Tests besteht, wird er von Microsoft »signiert«. Das bedeutet, dass das WHQL einen Hash bestimmt – einen eindeutigen Wert, der die Treiberdateien einschließlich der Abbilddatei darstellt Der PnP-Manager Kapitel 6 659
– und dann mit dem privaten Treibersignierschlüssel von Microsoft kryptografisch ­signiert. Der signierte Hash wird in einer Katalogdatei gespeichert und in das Windows-Installationsmedium aufgenommen bzw. an den Hersteller übermittelt, der den Treiber für sein Gerät eingereicht hat. Experiment: Katalogdateien einsehen Bei der Installation eines Treibers oder einer anderen Komponente, die eine Katalogdatei enthält, kopiert Windows diese Datei in ein Unterverzeichnis von %SystemRoot%\System32\ Catroot. Wenn Sie sich dieses Verzeichnis im Explorer ansehen, finden Sie dort ein Unterverzeichnis mit den .cat-Dateien. Beispielsweise sind in Nt5.cat und Nt5ph.cat die Signaturen und Seitenhashes für Windows-Systemdateien gespeichert. Wenn Sie eine Katalogdatei öffnen, erscheint ein Dialogfeld mit zwei Registerkarten. Die Registerkarte Allgemein gibt Informationen über die Signatur in der Katalogdatei, während die Registerkarte Sicherheitskatalog die Hashes der Komponenten enthält, die mit der Katalogdatei signiert sind. Der folgende Screenshot zeigt den Hash für die .sys-Datei eines Intel-Audiotreibers. Die anderen Hashes in dem Katalog gehören zu den Unterstützungs-DLLs, die mit dem Treiber ausgeliefert werden. Bei der Installation eines Treibers entnimmt der PnP-Manager des Benutzermodus die Signatur aus der zugehörigen Katalogdatei, entschlüsselt sie mit dem öffentlichen Treibersignierschlüssel von Microsoft und vergleicht den resultierenden Hash mit einem Hash der Treiberdatei, die er installieren will. Stimmen die Hashes überein, ist damit bestätigt, dass der Treiber den WHQL-Test bestanden hat. Schlägt die Verifizierung der Signatur dagegen fehl, handelt der PnP-Manager des Benutzermodus, wie es in der Treibersignierrichtlinie des Systems vorgesehen ist. Dabei besteht die Möglichkeit, dass er den Installationsversuch abbricht, den Benutzer vor dem nicht signierten Treiber warnt oder den Treiber trotzdem stillschweigend installiert. 660 Kapitel 6 Das E/A-System
Werden Treiber von Setupprogrammen installiert, die die Registrierung manuell konfigurieren und die Treiberdateien in das System kopieren, oder werden Treiberdateien dynamisch von Anwendungen geladen, so erfolgt keine Signaturprüfung nach der Signierrichtlinie des PnP-Managers. Stattdessen wird die Codesignierrichtlinie des Kernelmodus aus Kapitel 8 in Band 2 herangezogen. Nur bei Treibern, die mithilfe von INF-Dateien installiert werden, wird eine Validierung anhand der Treibersignierrichtlinie des PnP-Managers durchgeführt. Der PnP-Manager des Benutzermodus prüft auch, ob sich der zu installierende Treiber auf der von Windows Update unterhaltenen Treiberschutzliste befindet. Wenn ja, wird die Installation blockiert und eine Warnung an den Benutzer ausgegeben. Diese Liste enthält Treiber, von denen bekannt ist, dass sie Inkompatibilitäten oder Bugs aufweisen. Laden und Installieren von Treibern Im vorhergehenden Abschnitt haben Sie gesehen, wie der PnP-Manager Treiber für Hardwaregeräte findet und lädt. Diese Treiber werden meistens bei Bedarf geladen, also erst dann, wenn ein entsprechendes Gerät in dem System erscheint. Nachdem alle von einem Treiber verwalteten Geräte entfernt wurden, wird auch der Treiber entladen. Der Softwareschlüssel in der Registrierung enthält die Einstellungen für Treiber – und für Windows-Dienste, bei denen es sich aber um Benutzermodusprogramme handelt, die keine Verbindung zu den Kerneltreibern haben (wobei der Dienststeuerungs-Manager jedoch sowohl Dienste als auch Gerätetreiber laden kann). In diesem Abschnitt konzentrieren wir uns auf die Treiber. Eine ausführliche Erörterung der Dienste finden Sie in Kapitel 9 von Band 2. Treiber laden Jeder Unterschlüssel des Softwareschlüssels (HKLM\System\CurrentControlSet\Services) enthält einen Satz von Werten, die einige statische Aspekte eines Treibers (oder Dienstes) steuern. Einen dieser Werte, nämlich ImagePath, haben Sie bereits kennengelernt, als wir uns den Ladevorgang für PnP-Treiber angesehen haben. Abb. 6–36 zeigt ein Beispiel für einen Treiberschlüssel, und Tabelle 6–8 gibt einen Überblick über die wichtigsten Werte im Softwareschlüssel eines Treibers. (Eine vollständige Liste erhalten Sie in Kapitel 9 von Band 2.) Der Wert Start gibt an, in welcher Phase der Treiber (oder Dienst) geladen wird. In dieser Hinsicht unterscheiden sich Gerätetreiber und Dienste vor allem in den beiden folgenden Punkten: QQ Nur Gerätetreiber können die Start-Werte BOOT_START (0) und SYSTEM_START (1) aufweisen, da es in diesen Phasen noch keinen Benutzermodus gibt, weshalb Dienste nicht geladen werden können. Laden und Installieren von Treibern Kapitel 6 661
QQ Gerätetreiber können die Werte Group und Tag verwenden (in Tabelle 6–8 nicht gezeigt), um die Reihenfolge festzulegen, in der sie in der Startphase geladen werden. Im Gegensatz zu Diensten können sie aber die Werte DependOnGroup und DependOnService nicht nutzen. (Mehr dazu erfahren Sie in Kapitel 9 von Band 2.) Name des Werts Beschreibung ImagePath Der Pfad zur Abbilddatei (.sys-Datei) des Treibers. Type Gibt an, ob der Schlüssel für einen Dienst oder einen Treiber steht. Der Wert 1 bezeichnet einen Treiber, der Wert 2 einen Dateisystem- oder Filtertreiber. Die Werte 16 (0x10) und 30 (0x20) kennzeichnen Dienste. Mehr darüber erfahren Sie in Kapitel 9 von Band 2. Start Gibt an, wann der Treiber geladen werden soll. Hierzu gibt es folgende Op­ tionen: 0 (SERVICE_BOOT_START) Der Treiber wird vom Bootloader geladen. 1 (SERVICE_SYSTEM_START) Der Treiber wird nach der Initialisierung der Exekutive geladen. 2 (SERVICE_AUTO_START) Der Treiber wird vom Systemsteuerungs-Manager geladen. 3 (SERVICE_DEMAND_START) Der Treiber wird bei Bedarf geladen. 4 (SERVICE_DISABLED) Der Treiber wird nicht geladen. Tabelle 6–8 Wichtige Werte im Registrierungsschlüssel für einen Treiber Kapitel 11 in Band 2 beschreibt die Phasen des Bootvorgangs. Ein Treiber, der einen Start-Wert von 0 hat, wird vom Bootloader des Betriebssystems geladen, ein Treiber mit einem Start von 1 dagegen vom E/A-Manager, nachdem die Initialisierung des Exekutivteilsystems abgeschlossen ist. Der E/A-Manager ruft die Treiberinitialisierungsroutinen in der Reihenfolge auf, in der die Treiber in der jeweiligen Bootphase geladen werden. Ebenso wie bei Windows-Diensten kann auch in den Registrierungsschlüsseln von Treibern ein Group-Wert vorhanden sein, um die Gruppe anzugeben, zu der der Treiber gehört. Der Registrierungswert HKLM\SYSTEM\CurrentControlSet\Control\ServiceGroupOrder\List bestimmt die Reihenfolge, in der die Gruppen in der jeweiligen Bootphase geladen werden. Um die Ladereihenfolge noch genauer zu bestimmen, kann ein Treiber seine Anordnung innerhalb der Gruppe mit einem Tag-Wert festlegen. Der E/A-Manager sortiert die Treiber innerhalb der einzelnen Gruppen jeweils nach den Tag-Werten in den Registrierungsschlüsseln der Treiber, wobei Treiber ohne einen solchen Wert ans Ende ihrer Gruppe gestellt werden. Man könnte annehmen, dass der E/A-Manager Treiber mit niedrigerer Tag-Nummer vor denen mit höherer Tag-Nummer initialisiert, aber das muss nicht unbedingt der Fall sein. Der Registrierungsschlüssel HKLM\SYSTEM\CurrentControlSet\Control\GroupOrderList definiert die Tag-Reihenfolge innerhalb einer Gruppe. Dank dieses Schlüssels können Microsoft und Gerätetreiberentwickler jeweils ihr eigenes Nummerierungssystem definieren. Die Werte Group und Tags stammen noch aus der Frühzeit von Windows NT. In der Praxis werden sie heutzutage kaum noch genutzt. Die Treiber sollten nach Möglichkeit keine Abhängigkeiten von anderen Treibern haben (außer von den Kernelbibliotheken, mit denen sie verlinkt sind, z. B. NDIS.sys). 662 Kapitel 6 Das E/A-System
Der Start-Wert von Treibern wird nach den folgenden Richtlinien festgelegt: QQ Nicht-Plug-&-Play-Treiber setzen ihren Start-Wert auf die Bootphase, in der sie geladen werden sollen. QQ Treiber gleich welcher Art, die beim Systemstart vom Bootloader geladen werden sollen, müssen den Start-Wert BOOT_START (0) angeben. Dazu gehören u. a. die Systembustreiber und die Treiber für das Bootdateisystem. QQ Treiber, die zum Hochfahren des Systems nicht benötigt werden, sondern dazu da sind, Geräte zu erkennen, die vom Systembustreiber nicht aufgelistet werden können, haben den Start-Wert SYSTEM_START (1). Ein Beispiel dafür bildet der Treiber für den seriellen Port. Er informiert den PnP-Treiber über das Vorhandensein von seriellen PC-Standardports, die von Setup erkannt und in der Registrierung aufgezeichnet wurden. QQ Nicht-Plug-&-Play-Treiber und Dateisystemtreiber, die beim Systemstart nicht vorhanden sein müssen, haben den Start-Wert AUTO_START (2). Ein Beispiel dafür ist der MUP-Treiber (Multiple UNC Provider), der Unterstützung für UNC-Pfadnamen (Universal Naming Convention) für Remoteressourcen bietet (z. B. \\Remotecomputername\Freigabename). QQ Plug-&-Play-Treiber, die zum Hochfahren des Systems nicht benötigt werden, haben den Start-Wert DEMAND_START (3). Das trifft beispielsweise auf Netzwerkkartentreiber zu. Die Start-Werte für Plug-&-Play-Treiber und für die Treiber von Geräten, die aufgelistet werden können, dienen nur einem einzigen Zweck, nämlich dafür zu sorgen, dass der Lader des Betriebssystems die Treiber lädt, die zum erfolgreichen Hochfahren des Systems erforderlich sind. Die weitergehende Ladereihenfolge von Plug-&-Play-Treibern wird durch den Geräteauflistungsprozess des PnP-Managers bestimmt. Treiberinstallation Wie Sie gesehen haben, ist für die Installation von Plug-&-Play-Treibern eine INF-Datei nötig. Sie enthält die IDs der Hardwaregeräte, mit denen der Treiber umgehen kann, sowie Anweisungen, um Dateien zu kopieren und Registrierungswerte festzulegen. Auch für andere Arten von Treibern (wie Dateisystemtreiber, Dateisystemfilter und Netzwerkfilter) sind INF-Dateien erforderlich, die eindeutige Werte für die jeweilige Art von Treiber enthalten. Reine Softwaretreiber (wie derjenige, den Process Explorer nutzt) können ebenfalls eine INF-Datei zur Installation nutzen, allerdings ist dies nicht unbedingt erforderlich. Sie lassen sich mit einem Aufruf der API CreateService installieren (oder mithilfe eines Wrapperprogramms wie Sc.exe), wie es die Anwendung Process Explorer macht, nachdem sie ihren Treiber aus einer Ressource innerhalb der ausführbaren Datei bezogen hat (sofern sie mit erhöhten Rechten ausgeführt wird). Wie der Name der API schon andeutet, kann sie sowohl zur Installation von Treibern als auch von Diensten verwendet werden, was durch eines ihrer Argumente bestimmt wird. Die anderen Argumente geben den Start-Wert sowie weitere Parameter an (siehe die Dokumentation des Windows SDK). Nachdem der Treiber (oder Dienst) installiert wurde, wird er mit einem Aufruf der Funktion StartService geladen, die wiederum (für einen Treiber) wie üblich DriverEntry aufruft. Laden und Installieren von Treibern Kapitel 6 663
Ein reiner Softwaretreiber erstellt gewöhnlich ein Geräteobjekt mit einem Namen, den seine Clients kennen. Beispielsweise legt Process Explorer das Gerät PROCEXP152 an, das anschließend von Process Explorer im Aufruf von CreateFile verwendet wird. Darauf folgen Aufrufe wie DeviceIOControl, um Anforderungen an den Treiber zu senden (die vom E/A-Manager in IRPs umgewandelt werden). Abb. 6–43 zeigt den symbolischen Link für das Process Explorer-Objekt im Verzeichnis \GLOBAL?? (die Namen in diesem Verzeichnis sind für Clients im Benutzermodus zugänglich), dargestellt im Sysinternals-Tool WinObj. Dieses Verzeichnis wird von der Anwendung Process Explorer erstellt, wenn sie zum ersten Mal mit erhöhten Rechten ausgeführt wird. Wie Sie sehen, zeigt der Link auf das echte Geräteobjekt im Verzeichnis \Device und trägt denselben Namen (was nicht erforderlich ist). Abbildung 6–43 Symbolischer Link und Gerätename für Process Explorer Windows Driver Foundation Die Windows Driver Foundation (WDF) ist ein Framework zur Entwicklung von Treibern, das gängige Aufgaben wie die korrekte Handhabung von Plug-&-Play- und Energie-IRPs vereinfacht. Zu WDF gehören das Kernel-Mode Driver Framework (KMDF) und das User-­Mode Driver Framework (UMDF). WDF ist jetzt quelloffen und auf https://github.com/­Microsoft/ Windows-Driver-Frameworks zu finden. Die Tabellen 6–9 und 6–10 zeigen, welche Windows-­ Versionen welche KMDF- bzw. UMDF-Versionen unterstützen (ab Windows 7). KMDF-Version Releasemethode Enthalten in Treiber laufen auf 1.9 Windows 7 WDK Windows 7 Windows XP und höher 1.11 Windows 8 WDK Windows 8 Windows Vista und höher 1.13 Windows 8.1 WDK Windows 8.1 Windows 8.1 und höher 1.15 Windows 10 WDK Windows 10 Windows 10, Windows Server 2016 664 Kapitel 6 Das E/A-System →
KMDF-Version Releasemethode Enthalten in Treiber laufen auf 1.17 Windows 10 Version 1511 WDK Windows 10 Version 1511 Windows 10 Version 1511 und höher, Windows Server 2016 1.19 Windows 10 Version 1607 WDK Windows 10 Version 1607 Windows 10 Version 1607 und höher, Windows Server 2016 Tabelle 6–9 KMDF-Versionen UMDF-Version Releasemethode Enthalten in Treiber laufen auf 1.9 Windows 7 WDK Windows 7 Windows XP und höher 1.11 Windows 8 WDK Windows 8 Windows Vista und höher 2.0 Windows 8.1 WDK Windows 8.1 Windows 8.1 und höher 2.15 Windows 10 WDK Windows 10 Windows 10 und höher, Windows Server 2016 2.17 Windows 10 Version 1511 WDK Windows 10 Version 1511 Windows 10 Version 1511 und höher, Windows Server 2016 2.19 Windows 10 Version 1607 WDK Windows 10 Version 1607 Windows 10 Version 1607 und höher, Windows Server 2016 Tabelle 6–10 UMDF-Versionen In Windows 10 wurde das Prinzip der Universaltreiber eingeführt, das schon kurz in Kapitel 2 erwähnt wurde. Diese Treiber nutzen einen gemeinsamen Satz von DDIs, die in verschiedenen Editionen von Windows 10 implementiert sind – von IoT Core über Mobile bis zu den Desktopversionen. Universaltreiber können mit KMDF, UMDF 2.x und WDM erstellt werden. In Visual Studio geht das sogar relativ einfach, wenn die Einstellung Zielplattform auf Universal gesetzt ist. In diesem Fall macht der Compiler auf jegliche DDIs aufmerksam, die für Universaltreiber ungeeignet sind. In UMDF 1.x wurde für die Programmierung von Treibern ein Modell auf der Grundlage von COM verwendet, was sich sehr stark von dem Programmiermodell von KMDF (das auf Objekten beruhendes C verwendet) unterscheidet. Mit Version 2 wurde UMDF an KMDF angepasst und bietet nun eine fast identische API, was den Gesamtaufwand für die Treiberentwicklung in WDF verringert. Falls nötig, lassen sich UMDF-2.x-Treiber sogar mit wenig Mühe in KMDF-Treiber umwandeln. UMDF 1.x wird in diesem Buch nicht behandelt; weitere Informationen darüber entnehmen Sie bitte dem WDK. In den folgenden Abschnitten beschäftigen wir uns näher mit KMDF und UMDF. Beide verhalten sich unabhängig von dem Betriebssystem, auf dem sie laufen, jeweils gleichartig. Kernel-Mode Driver Framework Einige Einzelheiten von Windows Driver Foundation (WDF) haben Sie sich bereits in Kapitel 2 angesehen. In diesem Abschnitt werfen wir einen genaueren Blick auf die Komponenten und den Funktionsumfang von KMDF, also dem Teil des Frameworks für Kernelmodustreiber. Einige Aspekte der Kernarchitektur von KMDF werden wir hier aber nur kurz erwähnen. Eine ausführlichere Darstellung des Themas finden Sie in der Dokumentation des Windows Driver Kits. Windows Driver Foundation Kapitel 6 665
Die meisten Angaben in diesem Abschnitt gelten auch für UMDF 2.x. Die Ausnahmen werden im nachfolgenden Abschnitt beschrieben. Aufbau und Funktionsweise von KMDF-Treibern Sehen wir uns als Erstes an, was für Treiber und Geräte von KMDF unterstützt werden. Allgemein ausgedrückt sollte das jeder WDM-konforme Treiber sein, sofern er eine reguläre E/A-Verarbeitung und IRP-Bearbeitung durchführt. Nicht geeignet ist KMDF für Treiber, die die Windows-Kernel-API nicht direkt nutzen, sondern stattdessen Bibliotheksaufrufe an vorhandene Port- und Klassentreiber vornehmen. Da diese Treiber nur Callbacks für die WDM-Treiber bereitstellen, die die E/A-Verarbeitung tatsächlich durchführen, können sie KMDF nicht verwenden. Treiber, die sich nicht auf einen Port- oder Klassentreiber, IEEE1394, ISA, PCI-, PCMCIA und SD Client verlassen, sondern eigene Dispatchroutinen bereitstellen, können ebenfalls KMDF nutzen. KMDF bietet zwar eine Abstraktion oberhalb von WDM, doch die zuvor gezeigte grundlegende Treiberstruktur gilt auch für KMDF-Treiber. Sie müssen über folgende Funktionen verfügen: QQ Eine Initialisierungsroutine Wie alle anderen Treiber weisen auch KMDF-Treiber eine DriverEntry-Funktion auf, die den Treiber initialisiert. KMDF-Treiber starten das Framework an diesem Punkt und führen jegliche Konfigurations- und Initialisierungsschritte durch, die zu dem Treiber oder zur Beschreibung des Treibers gegenüber dem Framework gehören. Bei Nicht-Plug-&-Play-Treibern ist dies die Stelle, an der das erste Geräteobjekt erstellt werden sollte. QQ Eine Routine zum Hinzufügen eines Geräts Die Funktionsweise von KMDF-Treibern beruht auf Ereignissen und Callbacks (eine Beschreibung folgt in Kürze), wobei EvtDriverDeviceAdd der wichtigste Callback für PnP-Geräte ist, da er Benachrichtigungen empfängt, wenn der PnP-Manager eines der Geräte, für die der Treiber zuständig ist, im Kernel auflistet. QQ Eine oder mehrere EvtIo*-Routinen Ähnlich wie die Dispatchroutinen von WDM-Treibern kümmern sich diese Callbackroutinen um bestimmte Arten von E/A-Anforderungen von einer bestimmten Gerätewarteschlange. Ein Treiber legt gewöhnlich eine oder mehrere Warteschlangen an, in die KMDF E/A-Anforderungen für seine Geräte einreiht. Diese Warteschlangen können nach Anforderungs- und Dispatchtyp konfiguriert werden. Im einfachsten Fall kann ein KMDF-Treiber auch nur aus einer Initialisierungsroutine und einer Routine zum Hinzufügen von Geräten bestehen, da das Framework die allgemeine Funktonalität bereitstellt, die für die meisten Formen der E/A-Verarbeitung erforderlich ist, einschließlich Energie- und Plug-&-Play-Ereignisse. Im KMDF-Modell sind Ereignisse Laufzeitzustände, auf die ein Treiber reagieren oder an denen er teilnehmen kann. Diese Ereignisse haben nichts mit Synchronisierungsprimitiva zu tun (siehe Kapitel 8 in Band 2), sondern sind interne Konzepte des Frameworks. 666 Kapitel 6 Das E/A-System
Zur Handhabung von Ereignissen, die für das Funktionieren des Treibers von entscheidender Bedeutung sind oder die einer besonderen Verarbeitung bedürfen, kann der Treiber eine Callbackroutine registrieren. In anderen Fällen kann der Treiber das Framework eine Standardaktion ausführen lassen. Beispielsweise kann ein Treiber einen eigenen Callback für ein Auswerfen-Ereignis (EvtDeviceEject) bereitstellen, um das Auswerfen zu unterstützen, aber auch einfach den KMDF-Standardcode nutzen, der dem Benutzer mitteilt, dass ein Auswerfen bei dem betreffenden Gerät nicht möglich ist. Solche Standardverhalten gibt es jedoch nicht für alle Ereignisse, weshalb der Treiber dafür Callbacks bereitstellen muss. Ein bemerkenswertes Beispiel ist das zuvor beschriebene Ereignis EvtDriverDeviceAdd, das das Herzstück eines jeden Plug-&-Play-Treibers bildet. Experiment: KMDF- und UMDF-2-Treiber anzeigen Die mit den Debugging-Tools für Windows ausgelieferte Erweiterung Wdfkd.dll bietet viele Befehle zum Debuggen und Analysieren von KMDF-Treibern und Geräten (als Ersatz für die eingebaute Debuggingerweiterung im WDM-Stil, die nicht die gleiche Menge an WDF-spezifischen Informationen liefert). Installierte KMDF-Treiber können Sie mit dem Debuggerbefehl !wdfkd.wdfldr anzeigen lassen. Die folgende Beispielausgabe stammt von einer 32-Bit-VM mit Windows 10 in Hyper-V und zeigt die installierten mitgelieferten Treiber: lkd> !wdfkd.wdfldr --------------------------------------------------------------KMDF Drivers --------------------------------------------------------------LoadedModuleList 0x870991ec ---------------------------------LIBRARY_MODULE 0x8626aad8 Version v1.19 Service \Registry\Machine\System\CurrentControlSet\Services\Wdf01000 ImageName Wdf01000.sys ImageAddress 0x87000000 ImageSize 0x8f000 Associated Clients: 25 ImageName Ver umpass.sys v1.15 0xa1ae53f8 0xa1ae52f8 0x9e5f0000 0x00008000 WdfGlobals FxGlobals ImageAddress ImageSize peauth.sys v1.7 mslldp.sys v1.15 0x9aed1b50 0x9aed1a50 0x8e300000 0x00014000 vmgid.sys v1.15 0x97d0fd08 0x97d0fc08 0x8e260000 0x00008000 monitor.sys v1.15 0x97cf7e18 0x97cf7d18 0x8e250000 0x0000c000 tsusbhub.sys v1.15 0x97cb3108 0x97cb3008 0x8e4b0000 0x0001b000 NdisVirtualBus.sys v1.15 0x8d0fc2b0 0x8d0fc1b0 0x87a90000 0x00009000 vmgencounter.sys v1.15 0x8d0fefd0 0x8d0feed0 0x87a80000 0x00008000 0x95e798d8 0x95e797d8 0x9e400000 0x000ba000 → Windows Driver Foundation Kapitel 6 667
intelppm.sys v1.15 0x8d0f4cf0 0x8d0f4bf0 0x87a50000 0x00021000 vms3cap.sys v1.15 0x8d0f5218 0x8d0f5118 0x87a40000 0x00008000 netvsc.sys v1.15 0x8d11ded0 0x8d11ddd0 0x87a20000 0x00019000 hyperkbd.sys v1.15 0x8d114488 0x8d114388 0x87a00000 0x00008000 dmvsc.sys v1.15 0x8d0ddb28 0x8d0dda28 0x879a0000 0x0000c000 umbus.sys v1.15 0x8b86ffd0 0x8b86fed0 0x874f0000 0x00011000 CompositeBus.sys v1.15 0x8b869910 0x8b869810 0x87df0000 0x0000d000 cdrom.sys v1.15 0x8b863320 0x8b863220 0x87f40000 0x00024000 vmstorfl.sys v1.15 0x8b2b9108 0x8b2b9008 0x87c70000 0x0000c000 EhStorClass.sys v1.15 0x8a9dacf8 0x8a9dabf8 0x878d0000 0x00015000 vmbus.sys v1.15 0x8a9887c0 0x8a9886c0 0x82870000 0x00018000 vdrvroot.sys v1.15 0x8a970728 0x8a970628 0x82800000 0x0000f000 msisadrv.sys v1.15 0x8a964998 0x8a964898 0x873c0000 0x00008000 WindowsTrustedRTProxy.sys v1.15 0x8a1f4c10 0x8a1f4b10 0x87240000 0x00008000 WindowsTrustedRT.sys v1.15 0x8a1f1fd0 0x8a1f1ed0 0x87220000 0x00017000 intelpep.sys v1.15 0x8a1ef690 0x8a1ef590 0x87210000 0x0000d000 acpiex.sys v1.15 0x86287fd0 0x86287ed0 0x870a0000 0x00019000 ---------------------------------Total: 1 library loaded Sind UMDF-2.x-Treiber geladen, so werden sie ebenfalls angezeigt. Das ist einer der Vorteile der UMDF-2.x-Bibliothek. (Mehr darüber erfahren Sie im Abschnitt über UMDF weiter hinten in diesem Kapitel.) Die KMDF-Bibliothek ist in Wdf01000.sys implementiert, der aktuellen Version 1.x von KMDF. Zukünftige Versionen von KMDF können die Hauptversionsnummer 2 haben und in einem anderen Kernelmodul implementiert sein (Wdf02000.sys). Ein solches zukünftiges Modul kann sich parallel neben dem Modul der Version 1.x befinden, wobei beide jeweils die Treiber laden, die mit ihnen kompiliert wurden. Dadurch ist für eine Trennung und Unabhängigkeit von Treibern gesorgt, die mit KMDF-Bibliotheken verschiedener Hauptversionen erstellt wurden. Das Objektmodell von KMDF Ähnlich wie das Modell für den Kernel verwendet das Objektmodell von KMDF Eigenschaften, Methoden und Ereignisse, die in C implementiert sind, greift aber nicht auf den Objekt-Manager zurück. Stattdessen verwaltet KMDF seine eigenen Objekte intern und stellt sie Treibern als Handles bereit, wobei es die eigentlichen Datenstrukturen verborgen hält. Für jeden Objekttyp bietet das Framework Routinen, um Operationen an den Objekten auszuführen (Methoden), beispielsweise WdfDeviceCreate, die ein Gerät erstellt. Außerdem können Objekte über Datenfelder und Elemente verfügen, die über Get/Set (für Änderungen, die nicht fehlschlagen dürfen) oder Assing/Retrieve (für Änderungen, die fehlschlagen können) zugänglich sind. Diese Felder werden als Eigenschaften bezeichnet. Beispielsweise gibt die 668 Kapitel 6 Das E/A-System
Funktion WdfInterruptGetInfo Informationen über ein gegebenes Interruptobjekt zurück (WDFINTERRUPT). Anders als Kernelobjekte, die alle auf individuelle, isolierte Objekttypen verweisen, gehören sämtliche KMDF-Objekte zu einer Hierarchie, in der die meisten Objekttypen an ein Elternobjekt gebunden sind. Das Stammobjekt ist die WDFDRIVER-Struktur, die den eigentlichen Treiber beschreibt. Diese Struktur und ihre Bedeutung entsprechen der vom E/A-Manager bereitgestellten DRIVER_OBJECT-Struktur, und alle anderen KMDF-Strukturen sind Kinder davon. Das nächste wichtige Objekt ist WDFDEVICE. Es verweist auf eine bestimmte Instanz eines erkannten Geräts im System, das mit WdfDeviceCreate erstellt worden sein muss. Dies entspricht wiederum der DEVICE_OBJECT-Struktur, die im WDM-Modell und vom E/A-Manager verwendet wird. In Tabelle 6–11 sind die von KMDF unterstützten Objekttypen aufgeführt. Objekt Typ Beschreibung Kindliste WDFCHILDLIST Eine Liste von WDFDEVICE-Kindobjekten, die mit dem Gerät verbunden sind. Wird nur von Bustreibern verwendet. Sammlung WDFCOLLECTION Eine Liste von Objekten ähnlichen Typs, z. B. einer Gruppe von gefilterten WDFDEVICE-Objekten. Verzögerter Prozeduraufruf WDFDPC Eine Instanz eines DPC-Objekts Gerät WDFDEVICE Eine Geräteinstanz Gemeinsamer DMA-Puffer WDFCOMMONBUFFER Ein Bereich im Arbeitsspeicher, auf den ein Gerät und ein Treiber für DMA zugreifen können DMA-Aktivierung WDFDMAENABLER Aktiviert DMA auf einem gegebenen Kanal für einen Treiber. DMA-Transaktion WDFDMATRANSACTION Eine Instanz einer DMA-Transaktion Treiber WDFDRIVER Ein Objekt für den Treiber. Es stellt den Treiber, seine Parameter, seine Callbacks usw. dar. Datei WDFFILEOBJECT Die Instanz eines Dateiobjekts, das als Kanal für die Kommunikation zwischen einer Anwendung und dem Treiber verwendet werden kann Allgemeines Objekt WDFOBJECT Ermöglicht es, vom Treiber definierte eigene Daten als Objekt in das Objektdatenmodell des Frameworks einzubinden. Interrupt WDFINTERRUPT Eine Instanz eines Interrupts, den der Treiber handhaben muss. E/A-Warteschlange WDFQUEUE Dieses Objekt steht für eine gegebene E/A-­ Warteschlange. E/A-Anforderung WDFREQUEST Dieses Objekt steht für eine gegebene Anforderung in einer WDFQUEUE. E/A-Ziel WDFIOTARGET Dieses Objekt steht für den Gerätestack, der das Ziel einer gegebenen WDFREQUEST ist. Look-Aside-Liste WDFLOOKASIDE Beschreibt eine Look-Aside-Liste der Exekutive (siehe Kapitel 5). Windows Driver Foundation Kapitel 6 → 669
Objekt Typ Beschreibung Arbeitsspeicher WDFMEMORY Beschreibt eine Region des ausgelagerten oder nicht ausgelagerten Pools. Registrierungsschlüssel WDFKEY Beschreibt einen Registrierungsschlüssel. Ressourcenliste WDFCMRESLIST Gibt die einem WDFDEVICE zugewiesenen Hardwareressourcen an. Ressourcenbereichsliste WDFIORESLIST Gibt die möglichen Hardwareressourcenbereiche für ein WDFDEVICE an. Ressourcenerfordernisliste WDFIORESREQLIST Enthält ein Array aus WDFIORESLIST-Objekten, die alle möglichen Ressourcenbereiche für ein WDFDEVICE beschreiben. Spinlock WDFSPINLOCK Beschreibt ein Spinlock. String WDFSTRING Beschreibt eine Unicode-Stringstruktur. Timer WDFTIMER Beschreibt einen Exekutivtimer (siehe Kapitel 8 in Band 2). USB-Gerät WDFUSBDEVICE Bezeichnet eine Instanz eines USB-Geräts. USB-Schnittstelle WDFUSBINTERFACE Bezeichnet eine Schnittstelle des gegebenen WDFUSBDEVICE. USB-Pipe WDFUSBPIPE Bezeichnet eine Pipe zu einem Endpunkt auf einem gegebenen WDFUSBDEVICE. Wartesperre WDFWAITLOCK Dieses Objekt steht für ein Kerneldispatcher-Ereignisobjekt. WMI-Instanz WDFWMIINSTANCE Dieses Objekt stellt einen WMI-Datenblock für einen gegebenen WDFWMIPROVIDER dar. WMI-Anbieter WDFWMIPROVIDER Beschreibt das WMI-Schema für alle von dem Treiber unterstützten WDFWMIINSTANCE-Objekte. Arbeitselement WDFWORKITEM Beschreibt ein Exekutiv-Arbeitsobjekt. Tabelle 6–11 KMDF-Objekttypen An jedes dieser Objekte können andere KMDF-Objekte als Kindobjekte angehängt werden. Für einige Objekte gibt es nur ein oder zwei gültige Elternobjekte, während andere an jedes beliebige Elternobjekt angehängt werden können. So muss ein WDFINTERRUPT-Objekt beispielsweise immer mit einem gegebenen WDFDEVICE verknüpft werden, wohingegen ein WDFSPINLOCKoder ein WDFSTRING-Objekt beliebige Elternobjekte aufweisen kann. Das ermöglicht eine feine Steuerung ihrer Gültigkeit und Verwendung und eine Reduzierung globaler Statusvariablen. Abb. 6–44 zeigt die gesamte KMDF-Objekthierarchie. 670 Kapitel 6 Das E/A-System
(Treiber erstellt) (von der Warteschlange ausgeliefert) Vordefiniert Standardbeziehung; der Treiber kann aber auch zu jedem anderen Objekt wechseln Beide können Elternobjekte sein Abbildung 6–44 KMDF-Objekthierarchie Diese Beziehungen zwischen den Objekten müssen nicht unbedingt direkt sein. Es muss sich lediglich ein entsprechendes Elternobjekt in der Hierarchiekette befindet, das heißt, einer der Vorfahrenknoten muss von diesem Typ sein. Die Einrichtung solcher Beziehungen ist sehr praktisch, da sich Objekthierarchien nicht nur auf die Platzierung eines Objekts auswirken, sondern auch auf seine Lebensdauer. Wird ein Kindobjekt erstellt, so wird ihm über die Verknüpfung zu seinem Elternobjekt ein Referenzzähler hinzugefügt. Wenn das Elternobjekt zerstört wird, werden daher auch alle Kindobjekte zerstört. Durch die Verknüpfung von Objekten wie WDFSTRING und WDFMEMORY mit einem gegebenen Objekt statt mit dem standardmäßigen WDFDRIVER-Objekt ist es daher möglich, automatisch Speicher und Statusinformationen freizugeben, wenn das Elternobjekt zerstört wird. Eng verwandt mit dem Prinzip der Hierarchie ist der Objektkontext in KMDF. Da KMDF-Objekte wie bereits erwähnt undurchsichtig sind und zu ihrer Platzierung mit einem Elternobjekt verknüpft werden, ist es wichtig, dass Treiber ihre eigenen Daten an ein Objekt anhängen können, um Informationen zu verfolgen, die außerhalb der Möglichkeiten oder der Unterstützung des Frameworks liegen. Mithilfe von Objektkontexten können alle KMDF-Objekte solche Informationen enthalten. Außerdem lassen sie mehrere Objektkontextbereiche zu, sodass verschiedene Codeschichten innerhalb eines Treibers auf unterschiedliche Weise mit demselben Objekt umgehen können. In WDM ist es mithilfe der benutzerdefinierten Datenstruktur der Gerät­ erweiterung möglich, solche Informationen mit einem gegebenen Gerät zu verknüpfen, aber in KMDF können sogar Spinlocks und Strings Kontextbereiche enthalten. Diese Erweiterbarkeit erlaubt es jeder Bibliothek und jeder Codeschicht, die für die Verarbeitung einer E/A-Anfor- Windows Driver Foundation Kapitel 6 671
derung zuständig ist, unabhängig von anderem Code auf der Grundlage des Kontextbereichs vorzugehen, mit dem sie arbeitet. KMDF-Objekte weisen außerdem Attribute auf (siehe Tabelle 6–12). Sie werden gewöhnlich auf ihre Standardwerte eingestellt, doch wenn der Treiber das Objekt erstellt, kann er diese Werte durch Angabe einer WDF_OBJECT_ATTRIBUTES-Struktur überschreiben (ähnlich wie die OBJECT_ATTRIBUTES-Struktur, die der Objekt-Manager beim Erstellen eines Kernelobjekts verwendet). Attribut Beschreibung ContextSizeOverride Die Größe des Objektkontextbereichs ContextTypeInfo Der Typ des Objektkontextbereichs EvtCleanupCallback Der Callback, der den Treiber vor dem Löschen über die Bereinigung des Objekts informierten muss (wobei nach wie vor Verweise vorhanden sein können) EvtDestroyCallback Der Callback, der den Treiber über die unmittelbar bevorstehende Löschung des Objekts informierten muss (wobei der Referenzzähler 0 ist) ExecutionLevel Gibt die höchste IRQL an, auf der Callbacks von KMDF aufgerufen werden können. ParentObject Gibt das Elternobjekt an. SynchronizationScope Gibt an, ob Callbacks mit dem Elternobjekt, einer Warteschlange, einem Gerät oder gar nicht synchronisiert werden sollen. Tabelle 6–12 KMDF-Objektattribute Das E/A-Modell von KMDF Das E/A-Modell von KMDF folgt den in diesem Kapitel bereits beschriebenen WDM-Mechanismen. Sie können sich das Framework sogar als einen WDM-Treiber vorstellen, da es Kernel-APIs und WDM-Verhalten nutzt, um KMDF zu abstrahieren und funktionaler zu machen. Unter KMDF richtet der Frameworktreiber seine eigenen WDM-artigen IRP-Dispatchroutinen ein und übernimmt die Kontrolle über alle an den Gerätetreiber gesendete IRPs. Nachdem er von einem der drei E/A-Handler von KMDF behandelt wurde, verpackt er diese Anforderungen in geeignete KMDF-Objekte, stellt diese in die entsprechenden Warteschlangen (falls erforderlich) und führt einen Treibercallback aus, falls der Gerätetreiber an diesen Ereignissen interessiert ist. Abb. 6–45 zeigt den E/A-Fluss im Framework. 672 Kapitel 6 Das E/A-System
E/A-Warteschlangen ohne Energieverwaltung E/AAnforderungshandler Treibercallbacks E/A-Ziel E/A-Warteschlangen mit Energieverwaltung IRPs Dispatcher Handler für PnP-/ Energie­anforderungen Treibercallbacks Handler für WMIAnforderungen Treibercallbacks Abbildung 6–45 E/A-Fluss und IRP-Verarbeitung in KMDF Auf der Grundlage der bereits für WDM-Treiber beschriebenen IRP-Verarbeitung führt KMDF eine der drei folgenden Aktionen aus: QQ Es sendet das IRP an den E/A-Handler, der Standardgeräteoperationen verarbeitet. QQ Es sendet das IRP an den PnP- und Energie-Handler, der diese Arten von Ereignissen verarbeitet und andere Treiber benachrichtigt, wenn sich der Status geändert hat. QQ Es sendet das IRP an den WMI-Handler, der sich um Nachverfolgung und Protokollierung kümmert. Diese Komponenten benachrichtigen anschließend den Treiber über jegliche Ereignisse, für die er sich registriert hat, leiten die Anforderung ggf. zur weiteren Verarbeitung an einen anderen Handler weiter und schließen sie dann auf der Grundlage einer internen Handleraktion oder eines Treiberaufrufs ab. Wenn KMDF die Verarbeitung des IRPs beendet hat, die Anforderung selbst aber noch nicht vollständig verarbeitet wurde, ergreift KMDF eine der folgenden Maßnahmen: QQ Für Bus- und Funktionstreiber schließt es das IRP mit STATUS_INVALID_DEVICE_REQUEST ab. QQ Für Filtertreiber leitet es die Anforderung an den nächsttieferen Treiber weiter. Die E/A-Verarbeitung durch KMDF basiert auf einem Warteschlangenmechanismus (WDFQUEUE, nicht das zuvor in diesem Kapitel beschriebene KQUEUE-Objekt). KMDF-Warteschlangen sind hochgradig skalierbare Container für E/A-Anforderungen (verpackt als WDFREQUEST-Objekte) und bieten einen reichhaltigen Funktionsumfang, der über die bloße Sortierung der ausstehenden E/A-Operationen für ein gegebenes Objekt hinausgeht. Beispielsweise merken sich Warteschlangen die zurzeit aktiven Anforderungen und ermöglichen den Abbruch von E/A-Vorgängen, die gleichzeitige Ausführung und Beendigung von mehr als einer E/A-Anforderung auf einmal und die E/A-Synchronisierung (wie in der Liste der Objektattribute in Tabelle 6–12 Windows Driver Foundation Kapitel 6 673
bereits erwähnt). Ein typischer KMDF-Treiber erstellt mindestens eine Warteschlange und verknüpft mit jeder davon ein oder mehrere Ereignisse sowie einige der folgenden Optionen: QQ Die Callbacks, die für die mit dieser Warteschlange verknüpften Ereignisse registriert sind. QQ Der Energieverwaltungsstatus für die Warteschlange. KMDF unterstützt Warteschlangen mit und ohne Energieverwaltung. Bei Ersteren weckt der E/A-Handler das Gerät, wenn es erforderlich (und möglich) ist, stellt den Leerlauftimer scharf, wenn sich keine E/A-Opera­ tionen für das Gerät in der Warteschlange befinden, und ruft die E/A-Abbruchroutinen des Treibers auf, wenn das System den Arbeitszustand verlässt. QQ Die Dispatchmethode für die Warteschlange. KMDF kann E/A-Operationen aus einer Warteschlange im sequenziellen, parallelen und manuellen Modus ausliefern. Im sequenziellen Modus werden die E/As eine nach der anderen ausgeliefert (dabei wartet KMDF darauf, dass der Treiber die vorherige Anforderung abschließt), im parallelen werden sie dagegen an den Treiber geliefert, sobald es möglich ist. Im manuellen Modus muss der Treiber sie manuell aus der Warteschlange abrufen. QQ Die Angabe, ob die Warteschlange Puffer der Länge null akzeptiert, also etwa eingehende Anforderungen, die keine Daten enthalten. Die Dispatchmethode wirkt sich nur darauf aus, wie viele Anforderungen in der Warteschlange des Treibers auf einmal aktiv sein können. Sie legt nicht fest, ob die Ereigniscallbacks selbst gleichzeitig oder seriell aufgerufen werden. Letzteres wird durch das bereits erwähnte Objektattribut für den Synchronisierungsbereich bestimmt. Daher ist es möglich, dass bei einer parallelen Warteschlange die Gleichzeitigkeit ausgeschaltet ist, aber dennoch mehrere Anforderungen eingehen können. Auf der Grundlage des Warteschlangenmechanismus kann der E/A-Handler von KMDF beim Empfang einer Erstell-, Schließungs-, Bereinigungs-, Lese- oder Gerätesteuerungsanforderung (IOCTL) verschiedene Aufgaben ausführen: QQ Für Erstellanforderungen kann der Treiber verlangen, sofort über das Callbackereignis EvtDeviceFileCreate benachrichtigt zu werden. Er kann auch eine nicht manuelle Warteschlange für den Empfang von Erstellanforderungen anlegen und einen EvtIoDefault-­ Callback registrieren, um die Benachrichtigungen zu erhalten. Wird keine dieser Methoden verwendet, schließt KMDF die Anforderung einfach mit einem Erfolgscode ab. Das bedeutet, dass Anwendungen standardmäßig Handles für KMDF-Treiber öffnen können, die keinen eigenen Code bereitstellen. QQ Für Bereinigungs- und Schließungsanforderungen wird der Treiber unmittelbar über die Callbacks EvtFileCleanup bzw. EvtFileClose informiert, sofern er dafür registriert ist. Anderenfalls schließt das Framework die Anforderung einfach mit einem Erfolgscode ab. QQ Für Schreib-, Lese- und IOCTL-Anforderungen erfolgt der Vorgang aus Abb. 6–46. 674 Kapitel 6 Das E/A-System
Hat der Treiber eine Warteschlange für diesen Anforderungstyp? Ist dies ein Filter­ treiber? J J Akzeptiert die Warteschlange Anforderungen? Übergibt die Anforderung an den nächsttieferen Treiber J Erstellt ein WDFREQUESTObjekt für die Anforderung Lässt die Anforderung fehlschlagen Unterliegt die Warteschlange der Energiever­waltung? J Befindet sich das Gerät im Arbeitsstatus? J Stellt die Anforderung in die Warteschlange Benachrichtigt den PnP-/ Energie-Handler, um das Gerät aufzuwecken Abbildung 6–46 Verarbeitung von Lese-, Schreib- und IOCTL-Anforderungen durch KMDF User-Mode Driver Framework Windows enthält eine wachsende Anzahl von Treibern, die im Benutzermodus laufen und das User-Mode Driver Framework (UMDF) verwenden, einen Teil der WDF. In UMDF Version 2 wurden das Objekt-, das Programmier- und das E/A-Modell an das von KMDF angepasst. Dennoch sind diese beiden Frameworks nicht identisch, da es schließlich grundlegende Unterschiede zwischen Benutzer- und Kernelmodus gibt. Beispielsweise gibt es einige der in Tabelle 6–12 aufgeführten KMDF-Objekte nicht in UMDF, darunter WDFCHILDLIST, Objekte im Zusammenhang mit DMA, WDFLOOKASIDELIST (Look-Aside-Listen können nur im Kernelmodus zugewiesen werden), WDFIORESLIST, WDFIORESREQLIST, WDFDPC sowie WMIK-Objekte. Die meisten Objekte und Prinzipien von KMDF gelten jedoch auch in UMDF 2.x. UMDF bietet mehrere Vorteile gegenüber KMDF: QQ UMDF werden im Benutzermodus ausgeführt, weshalb unbehandelte Ausnahmen nur den UMDF-Hostprozess zum Absturz bringen, aber nicht das gesamte System. QQ Der UMDF-Hostprozess läuft unter dem lokalen Dienstkonto, das sehr geringe Rechte auf dem lokalen Computer hat und nur anonym auf Netzwerkverbindungen zugreifen kann. Das verringert die Angriffsfläche. QQ Die Ausführung im Benutzermodus bedeutet, dass die IRQL stets 0 beträgt (PASSIVE_LEVEL). Der Treiber kann daher stets Seitenfehler entgegennehmen und Kerneldispatcherobjekte (Ereignisse, Mutexe usw.) zur Synchronisierung verwenden. Windows Driver Foundation Kapitel 6 675
QQ UMDF-Treiber lassen sich leichter debuggen als KMDF-Treiber, da hierzu keine zwei getrennten Computer (virtuell oder physisch) erforderlich sind. Der Hauptnachteil von UMDF besteht in der erhöhten Latenz aufgrund der Übergänge zwischen Kernel- und Benutzermodus und der erforderlichen Kommunikation (was wir in Kürze beschreiben werden). Außerdem sind einige Arten von Treibern, z. B. diejenigen für PCI-Hochgeschwindigkeitsgeräte, einfach nicht für die Ausführung im Benutzermodus gedacht und können daher nicht mit UMDF geschrieben werden. UMDF ist eigens zur Unterstützung von Protokollgeräteklassen ausgelegt. Solche Klassen umfassen jeweils alle Geräte, die dasselbe standardisierte, allgemeine Protokoll verwenden und auf dieser Grundlage eine besondere Funktionalität anbieten. Zu den Protokollen gehören zurzeit IEEE 1394 (FireWire), USB, Bluetooth und TCP/IP; des Weiteren gibt es noch eine Klasse für Geräte mit direkter Benutzerinteraktion (Human Interface Devices, HIDs; in Windows Eingabegeräte genannt). Alle Geräte, die auf einem dieser Busse laufen (oder an ein Netzwerk angeschlossen sind), sind potenzielle Kandidaten für UMDF, beispielsweise tragbare Musikabspielgeräte, Eingabegeräte, Mobiltelefone, Kameras, Webcams usw. UMDF wird außerdem von SideShow-kompatiblen Geräten (Hilfsanzeigen) sowie dem Windows Portable Device Framework (WPD) genutzt, das USB-Wechselspeichermedien unterstützt (USB-Massenübertragungsgeräte). Wie bei KMDF ist es in UMDF außerdem auch möglich, reine Softwaretreiber zu implementieren, z. B. für ein virtuelles Gerät. KMDF-Treiber werden als Treiberobjekte ausgeführt, die für eine SYS-Abbilddatei stehen. Im Gegensatz dazu laufen UMDF-Treiber in einem Treiberhostprozess, der das Image %SystemRoot%\System32\WUDFHost.exe ausführt und einem Serverhostprozess ähnelt. Dieser Hostprozess enthält den Treiber selbst, das UMDF (in Form einer DLL) sowie eine Laufzeitumgebung (zuständig für E/A-Disponierung, das Laden der Treiber, Gerätestackverwaltung, Kommunika­ tion mit dem Kernel und dem Threadpool). Wie im Kernel werden auch die einzelnen UMDF-Treiber als Teil eines Stacks ausgeführt. Er kann mehrere Treiber für die Verwaltung eines Geräts einschließen. Da Benutzermoduscode nicht auf den Kerneladressraum zugreifen kann, enthält UMDF auch Komponenten, die einen solchen Zugriff über eine besondere Schnittstelle erlauben. Dies wird durch einen Kernelmodus­ teil von UMDF erreicht, der ALPC einsetzt, einen Mechanismus zur Kommunikation zwischen Prozessen, der mit der Laufzeitumgebung in den Benutzermodus-Treiberhostprozessen spricht. (Mehr über ALPC erfahren Sie in Kapitel 8 von Band 2.) Abb. 6–47 zeigt die Architektur des UMDF-Treibermodells. 676 Kapitel 6 Das E/A-System
UMDF-Hostprozess Treiber-Manager UMDF-Treiber UMDF-Treiber UMDF-Framework Benutzermodus UMDF-Laufzeit Reflektor (Filter) Kernelmodustreiber UMDF-Hostprozess Anwendungen Win32-API Windows-Kernel Bereitgestellt durch: UMDF-Framework UMDF-Laufzeit Reflektor (Filter) Kernelmodustreiber Kernelmodustreiber Stack des logischen Geräts Stack des logischen Geräts Abbildung 6–47 UMDF-Architektur Abb. 6–47 zeigt zwei Gerätestacks zur Verwaltung von zwei verschiedenen Hardwaregeräten. In jedem davon läuft ein UMDF-Treiber jeweils in seinem eigenen Treiberhostprozess. In dem Diagramm sind die folgenden Bestandteile der Architektur zu sehen: QQ Anwendungen Sind die Clients des Treibers. Es handelt sich dabei um Windows-Standardanwendungen, die dieselben APIs für E/A-Vorgänge verwenden wie bei einem von KMDF oder WDM verwalteten Gerät. Die Anwendungen wissen nicht (oder kümmern sich nicht darum), dass sie mit einem UMDF-Gerät kommunizieren. Ihre Aufrufe werden nach wie vor an den E/A-Manager des Kernels gesendet. QQ Windows-Kernel (E/A-Manager) Wie auch für jedes andere Standardgerät erstellt der E/A-Manager auf der Grundlage der E/A-APIs der Anwendungen die IRPs für die Operationen. QQ Reflektor Der Reflektor ist der eigentliche Motor von UMDF. Es handelt sich dabei um einen standardmäßigen WDM-Filtertreiber (%SystemRoot%\System32\Drivers\WUDFRD. sys), der an der Spitze des Stacks für jedes von einem UMDF-Treiber verwalteten Geräts sitzt. Der Reflektor verwaltet die Kommunikation zwischen dem Kernel und dem Benutzermodus-Treiberhostprozess. IRPs im Zusammenhang mit Energieverwaltung, Plug & Play und Standard-E/A werden über ALPC an den Hostprozess weitergeleitet. Dadurch kann der UMDF-Treiber auf die E/A-Vorgänge reagieren und Arbeit leisten sowie sich durch Auflistung, Installation und Verwaltung seiner Geräte am Plug-&-Play-Modell beteiligen. Überdies behält der Reflektor die Treiberhostprozesse im Auge und sorgt dafür, dass sie in angemessener Zeit auf Anforderungen reagieren, damit die Treiber und Anwendungen nicht hängen. QQ Treiber-Manager Der Treiber-Manager ist dafür zuständig, die Treiberhostprozesse je nachdem zu starten und zu beenden, welche UMDF-verwalteten Geräte vorhanden sind, und die Informationen darüber zu verwalten. Er muss auch auf Meldungen vom Reflektor Windows Driver Foundation Kapitel 6 677
reagieren (z. B. auf eine Geräteinstallation) und sie auf die entsprechenden Hostprozesse anwenden. Ausgeführt wird der Treiber-Manager als ein in %SystemRoot%\System32\ WUDFsvc.dll implementierter Windows-Standarddienst in einem Svchost.exe-Standardhostprozess. Er ist so eingerichtet, dass er automatisch gestartet wird, sobald der erste UMDF-Treiber für ein Gerät installiert ist. Für sämtliche Treiberhostprozesse wird nur eine einzige Instanz des Treiber-Managers ausgeführt (wie es bei Diensten immer der Fall ist). Er muss stets laufen, damit die UMDF-Treiber arbeiten können. QQ Hostprozess Der Hostprozess stellt den Adressraum und die Laufzeitumgebung für den eigentlichen Treiber bereit (WUDFHost.exe). Obwohl er unter dem lokalen Dienstkonto ausgeführt wird, ist er kein Windows-Dienst und wird auch nicht von SCM verwaltet, sondern nur vom Treiber-Manager. Der Hostprozess stellt den Benutzermodus-Gerätestack für die Hardware bereit, der für alle Anwendungen im System sichtbar ist. Zurzeit hat jede Instanz eines Geräts ihren eigenen Gerätestack in einem eigenen Hostprozess. Es kann aber sein, dass sich in Zukunft mehrere Instanzen einen Hostprozess teilen. Hostprozesse sind Kindprozesse des Treiber-Managers. QQ Kernelmodustreiber Wenn für ein von einem UMDF-Treiber verwaltetes Gerät eine besondere Kernelunterstützung erforderlich ist, kann für diesen Zweck auch ein Kernelmodus-Begleittreiber geschrieben werden. Dadurch ist es möglich, das Gerät sowohl durch einen UMDF- als auch durch einen KMDF- (oder WDM-) Treiber zu verwalten. Um sich UMDF auf Ihrem System in Aktion anzusehen, müssen Sie lediglich einen nicht leeren USB-Stick daran anschließen. In Process Explorer wird der Treiberhostprozess WUDFHost.exe angezeigt. Wenn Sie zur DLL-Ansicht umschalten und nach untern scrollen, können Sie wie in Abb. 6–48 die DLLs erkennen. Abbildung 6–48 DLLs im UMDF-Hostprozess 678 Kapitel 6 Das E/A-System
Hier erkennen Sie drei der Hauptkomponenten aus der zuvor gegebenen Übersicht über die Architektur wieder: QQ WUDFHost.exe Dies ist die ausführbare Datei für den UMDF-Hostprozess. QQ WUDFx02000.dll Dies ist die DLL für UMDF 2.x. QQ WUDFPlatform.dll Dies ist die Laufzeitumgebung. Energieverwaltung Die Plug-&-Play-Funktionen von Windows erfordern Unterstützung durch die Hardware des Systems, und ebenso ist auch für Energieverwaltungsfunktionen Hardware erforderlich, die mit der ACPI-Spezifikation (Advanced Configuration and Power Interface) übereinstimmt. Diese Spezifikation gehört jetzt zum Unified Extensible Firmware Interface (UEFI) und steht auf http:// www.uefi.org/specifications zur Verfügung. Der ACPI-Standard definiert verschiedene Energiezustände für Systeme und Geräte. Die sechs Systemenergiezustände S0 (vollständig eingeschaltet) bis S5 (komplett aus) sind in Tabelle 6–13 aufgeführt. Sie zeichnen sich jeweils durch die folgenden Merkmale aus: QQ Energieverbrauch Der Betrag an Energie, den das System verbraucht. QQ Wiederaufnahmestatus Dies ist der Softwarestatus, zu dem das System zurückkehrt, wenn es zu einem »weiter eingeschalteten« Zustand wechselt. QQ Hardwarelatenz Die Zeit, die es dauert, um das System wieder in den vollständig eingeschalteten Zustand zu versetzen. Zustand Energieverbrauch Wiederaufnahmestatus Hardwarelatenz S0 (vollständig eingeschaltet) Maximum – Keine S1 (Energiesparmodus) Kleiner als in S0, größer als in S2 System nimmt den Zustand ein, den es verlassen hat (Rückkehr zu S0). Weniger als zwei Sekunden S2 (Energiesparmodus) Kleiner als in S1, größer als in S3 System nimmt den Zustand ein, den es verlassen hat (Rückkehr zu S0). Zwei oder mehr Sekunden S3 (Energiesparmodus) Kleiner als in S2; Prozessor ist aus. System nimmt den Zustand ein, den es verlassen hat (Rückkehr zu S0). Zwei oder mehr Sekunden S4 (Ruhezustand) Erhaltungsstrom für Einschalttaste und Aufweckschaltung Das System wird von der gespeicherten Ruhezustandsdatei neu gestartet und nimmt den Zustand ein, den es vor dem Ruhezustand hatte. Lang und nicht definiert S5 (komplett aus) Erhaltungsstrom für Einschalttaste Systemstart Lang und nicht definiert Tabelle 6–13 Systemenergiezustände Energieverwaltung Kapitel 6 679
Die Zustände S1 bis S4 sind Schlaf- oder Standbyzustände, in denen das System aufgrund seiner geringeren Leistungsaufnahme ausgeschaltet zu sein scheint. Dabei behält es jedoch ausreichend Informationen bei – im Arbeitsspeicher oder auf der Festplatte –, um wieder zu S0 zu wechseln. Bei den Zuständen S1 bis S3 ist der Energieverbrauch noch hoch genug, um den Inhalt des Arbeitsspeichers zu erhalten, sodass die Energieverwaltung die Ausführung beim Übergang zu S0 (wenn der Benutzer oder ein Gerät den Computer aufweckt) dort fortsetzt, wo sie sie beim Übergang in den Energiesparmodus abgebrochen hat. Beim Übergang zu S4 dagegen speichert die Energieverwaltung die komprimierten Inhalte des Arbeitsspeichers in der Ruhezustandsdatei Hiberfil.sys im Stammverzeichnis des Systemvolumes (verborgene Datei). Diese Datei ist groß genug, um auch die nicht komprimierten Inhalte des Arbeitsspeichers zu fassen, aber um die Festplatten-E/A zu minimieren und die Leistung beim Wechsel in und aus dem Ruhezustand zu verbessern, wird die Komprimierung eingesetzt. Nach diesem Speichervorgang schaltet die Energieverwaltung den Computer aus. Schaltet der Benutzer den Computer anschließend wieder ein, erfolgt ein normaler Startvorgang, allerdings mit der Ausnahme, dass der Boot-Manager das Speicherabbild in der Ruhezustandsdatei entdeckt. Ist in dieser Datei ein gespeicherter Systemstatus enthalten, startet der Boot-Manager %SystemRoot%\System32\Winresume.exe, die den Inhalt der Datei in den Arbeitsspeicher einliest und die Ausführung an der Stelle im Arbeitsspeicher fortsetzt, die in der Ruhezustandsdatei aufgezeichnet wurde. Auf Systemen mit hybridem Standbymodus resultiert die Anforderung, den Computer schlafen zu legen, in einer Kombination aus den Zuständen S3 und S4. Der Computer geht in den Energiesparmodus über, aber dabei wird auch eine Notfall-Ruhezustandsdatei auf die Festplatte geschrieben. Im Gegensatz zu regulären Ruhezustandsdateien, die fast den gesamten aktiven Arbeitsspeicher enthalten, befinden sich in dieser Datei nur die Daten, die später nicht eingelagert werden können. Da weniger Daten auf die Festplatte geschrieben werden, erfolgt der Übergang in diesen Modus schneller als der Wechsel in den regulären Ruhezustand. Dabei werden die Treiber darüber informiert, dass ein Übergang zu S4 ansteht, sodass sie sich wie bei einem normalen Ruhezustand selbst konfigurieren und ihren Status speichern können. Anschließend geht das System in den normalen Energiesparmodus über. Fällt dabei der Strom aus, befindet sich das System praktisch in einem S4-Zustand: Wenn der Benutzer den Computer wieder einschaltet, kann Windows die Ausführung auf der Grundlage der Notfall-Ruhezustandsdatei wieder aufnehmen. Sie können den Ruhezustand komplett ausschalten und damit etwas Festplattenplatz gewinnen, indem Sie an einer Eingabeaufforderung mit erhöhten Rechten powercfg /h off ausführen. Unmittelbare Übergänge zwischen den Status S1 bis S4 finden nie statt, da dazu eine Codeausführung erforderlich wäre, die CPU in diesen Zuständen aber ausgeschaltet ist. Stattdessen muss immer erst ein Übergang zu S0 erfolgen. Ein Übergang von irgendeinem der Zustände S1 bis S5 zu S0 wird als Aufwachen bezeichnet, ein Übergang von S0 zu einem der Zustände S1 bis S5 als Einschlafen (siehe Abb. 6–49). 680 Kapitel 6 Das E/A-System
Abbildung 6–49 Übergänge zwischen den Systemenergiezuständen Experiment: Systemenergiezustände Um sich die auf dem System verfügbaren Energiezustände anzusehen, öffnen Sie eine Eingabeaufforderung mit erhöhten Rechten und geben den Befehl powercfg /a ein. Dadurch erhalten Sie eine Ausgabe wie die folgende: C:\WINDOWS\system32>powercfg /a Die folgenden Standbymodusfunktionen sind auf diesem System verfügbar: Standby (S3) Ruhezustand Schnellstart Die folgenden Standbymodusfunktionen sind auf diesem System nicht verfügbar: Standby (S1) Die Systemfirmware unterstützt diesen Standbystatus nicht. Standby (S2) Die Systemfirmware unterstützt diesen Standbystatus nicht. Standby (S0 Niedriger Energiestand – Leerlauf) Die Systemfirmware unterstützt diesen Standbystatus nicht. Hybrider Standbymodus Der Hypervisor unterstützt diesen Standbystatus nicht. Hier sind der Standbystatus S3 und der Ruhezustand verfügbar. Im Folgenden schalten wir den Ruhezustand aus und geben den Befehl dann erneut ein: C:\WINDOWS\system32>powercfg /h off C:\WINDOWS\system32>powercfg /a → Energieverwaltung Kapitel 6 681
Die folgenden Standbymodusfunktionen sind auf diesem System verfügbar: Standby (S3) Die folgenden Standbymodusfunktionen sind auf diesem System nicht verfügbar: Standby (S1) Die Systemfirmware unterstützt diesen Standbystatus nicht. Standby (S2) Die Systemfirmware unterstützt diesen Standbystatus nicht. Ruhezustand Ruhezustand wurde nicht aktiviert. Standby (S0 Niedriger Energiestand – Leerlauf) Die Systemfirmware unterstützt diesen Standbystatus nicht. Hybrider Standbymodus Ruhezustand ist nicht verfügbar. Der Hypervisor unterstützt diesen Standbystatus nicht. Schnellstart Ruhezustand ist nicht verfügbar. Für Geräte sind in ACPI die vier Energiezustände D0 bis D3 definiert, wobei D0 »vollständig eingeschaltet« und D3 »komplett aus« ist. Die genaue Bedeutung der Zustände D1 und D2 festzulegen, überlässt der ACPI-Standard den einzelnen Treibern und Geräten. Vorgeschrieben ist dabei lediglich, dass der Energieverbrauch in D1 kleiner oder gleich dem in D0 und der Energieverbrauch in D2 kleiner oder gleich dem in D1 sein muss. In Windows 8 und höher ist D3 in die zwei Unterzustände aufgeteilt, den »heißen« und den »kalten« D3. Im heißen D3 ist das Gerät größtenteils ausgeschaltet, aber noch nicht von seiner Hauptstromquelle getrennt. Außerdem kann der Elternbuscontroller in diesem Zustand das Vorhandensein des Geräts auf dem Bus erkennen. Im kalten D3 dagegen hat das Gerät keine Verbindung mehr mit seiner Stromquelle, und der Buscontroller kann das Gerät auch nicht mehr wahrnehmen. Dieser Zustand bietet noch eine erweiterte Gelegenheit, um Strom zu sparen. Abb. 6–50 zeigt die Gerätezustände und die möglichen Statusübergänge. 682 Kapitel 6 Das E/A-System
heiß kalt Abbildung 6–50 Übergänge zwischen Gerätestatus Vor Windows 8 konnten Geräte den heißen D3-Zustand nur dann erreichen, wenn das System vollständig eingeschaltet war (S0). Der Übergang zum kalten D3 erfolgte implizit, wenn das System in einen Standbymodus überging. Ab Windows 8 kann ein Gerät auch bei vollständig eingeschaltetem System in den kalten D3-Zustand versetzt werden. Allerdings kann der Treiber, der das Gerät steuert, dies nicht direkt tun, sondern muss es zunächst in den heißen D3 versetzen. Der Bustreiber und die Firmware können anschließend entscheiden, alle Geräte an ihrem Bus, die sich im heißen D3-Zustand befinden, auf kalten D3 umzuschalten. Diese Entscheidung hängt von zwei Faktoren ab, nämlich erstens, ob der Bustreiber und die Firmware überhaupt dazu in der Lage sind, und zweitens, ob der Treiber einen Übergang in den kalten D3 vorgesehen hat, was er durch Angabe in der INF-Installationsdatei oder durch einen dynamischen Aufruf der Funktion SetD3DColdSupport tun kann. Zusammen mit anderen Originalgeräteherstellern hat Microsoft eine Reihe von Spezifikationen für die Energieverwaltung aufgestellt, die die für alle Geräte in einer bestimmten Klasse erforderlichen Geräteenergiezustände festlegen (für die Hauptgeräteklassen wie Anzeige, Netzwerk, SCSI usw.). Einige Geräte können nur entweder »vollständig ein« oder »vollständig aus« sein, weshalb jegliche Zwischenzustände bei ihnen nicht definiert sind. Verbundener und moderner Standbymodus In dem letzten Experiment ist Ihnen wahrscheinlich ein weiterer Systemzustand aufgefallen, der als Standby (S0 Niedriger Energiestand – Leerlauf) angezeigt wurde. Dies ist kein offizieller ACPI-Status, sondern eine Variante von S0, die in Windows 8.x als verbundener oder Verbindungsstandby bezeichnet wurde und in Windows 10 (sowohl in den Desktop- als auch den Mobileditionen) zum modernen Standbymodus erweitert wurde. Der reguläre Standbymodus S3 wird manchmal auch als veralteter oder Legacy-Standby bezeichnet. Das Hauptproblem bei S3 besteht darin, dass das System in diesem Zustand nicht arbeitet, sodass es beispielsweise eine eingehende E-Mail nicht aufnehmen kann, ohne in den S0-­Zustand aufzuwachen, was je nach Konfiguration und den Möglichkeiten der Geräte zwar möglich sein kann, aber nicht muss. Wenn das System tatsächlich aufwacht, um die E-Mail abzurufen, geht es danach aber nicht wieder sofort in den Standbymodus über. Durch den modernen Standbymodus werden beide Probleme gelöst. Energieverwaltung Kapitel 6 683
Systeme, die den modernen Standbymodus unterstützen, gehen in diesen Zustand über, wenn sie angewiesen werden, in den Standby zu wechseln. Technisch befindet sich das System immer noch im Status S0, da die CPU aktiv ist und Code ausgeführt werden kann. Allerdings sind Desktopprozesse (Nicht-UWP-Apps) und UWP-Apps angehalten (wobei sich die meisten UWP-Apps nicht im Vordergrund befinden und daher ohnehin angehalten sind). Von UWPApps erstellte Hintergrundtasks dagegen können weiterhin ausgeführt werden. So kann beispielsweise ein E-Mail-Client einen Hintergrundtask haben, der regelmäßig nach neuen Nachrichten Ausschau hält. Im modernen Standbymodus kann das System auch sehr schnell zu S0 aufwachen (was manchmal als »instant on« bezeichnet wird). Allerdings können nicht alle Systeme diesen Modus nutzen; dies hängt vom Chipset und anderen Komponenten der Plattform ab. (Das System, auf dem wir das letzte Experiment ausgeführt haben, unterstützt den modernen Standbymodus nicht, sondern dafür den regulären S3-Standbymodus.) Weitere Informationen über den modernen Standbymodus finden Sie in der Windows-Hardwaredokumentation auf https://msdn.microsoft.com/en-us/library/windows/hardware/mt2825​ 15(v=vs.85).aspx. Funktionsweise der Energieverwaltung Die Verantwortung für die Windows-Energierichtlinien ist auf die als Energieverwaltung (Power Manager) bezeichnete Komponente und die einzelnen Gerätetreiber aufgeteilt. Die Energieverwaltung ist Besitzer der Systemenergierichtlinie. Das bedeutet, dass die Energieverwaltung entscheidet, welcher Systemenergiezustand jeweils geeignet ist und wann das System in einen Energiesparmodus oder den Ruhezustand versetzt oder heruntergefahren werden soll. Sie weist auch die dazu fähigen Geräte im System an, die entsprechenden Übergänge zwischen den Systemenergiezuständen durchzuführen. Um zu entscheiden, welcher Übergang angebracht ist, zieht die Energieverwaltung die folgenden Faktoren heran: QQ Aktivitätsgrad auf dem System QQ Akkuladezustand QQ Anforderungen von Anwendungen, das System herunterzufahren oder in den Ruhezustand oder Energiesparmodus zu versetzen QQ Benutzeraktionen wie die Betätigung des Netzschalters QQ Energieeinstellungen in der Systemsteuerung Wenn der PnP-Manager eine Geräteauflistung durchführt, erhält er unter anderem auch Informationen über die Fähigkeiten der Geräte zur Energieverwaltung. Ein Treiber meldet es, wenn seine Geräte die Gerätezustände D1 und D2 unterstützen, und gibt optional auch die Latenzen, also die erforderlichen Zeiten für den Wechsel von D1 zu D3 bis D0 an. Um der Energieverwaltung bei der Entscheidung zu helfen, wann ein Energiezustandsübergang erfolgen soll, geben die Bustreiber auch eine Tabelle mit einer Zuordnung zwischen den Systemenergiezuständen S0 bis S5 und den vom Gerät unterstützten Geräteenergiezuständen zurück. Diese Tabelle nennt den jeweils geringstmöglichen Geräteenergiezustand für die einzel- 684 Kapitel 6 Das E/A-System
nen Systemzustände und spiegelt unmittelbar den Zustand der verschiedenen Energie­niveaus wieder für den Energiespar- oder Ruhemodus wider. Für einen Bus, der alle vier Geräteenergiezustände unterstützt, kann beispielsweise die Zuordnungstabelle aus Tabelle 6–14 zurückgegeben werden. Die meisten Gerätetreiber schalten ihre Geräte komplett aus (D3), wenn das System den Zustand S0 verlässt, um den Energieverbrauch zu minimieren, während der Computer nicht in Gebrauch ist. Andere Geräte dagegen haben die Möglichkeit, das System aus einem Standbyzustand aufzuwecken, beispielsweise Netzwerkkarten. Auch diese Fähigkeit und der niedrigste Geräteenergiezustand, bei dem sie noch vorhanden ist, werden bei der Geräteauflistung gemeldet. Systemenergiezustand Geräteenergiezustand S0 (vollständig eingeschaltet) D0 (vollständig eingeschaltet) S1 (Energiesparmodus) D1 S2 (Energiesparmodus) D2 S3 (Energiesparmodus) D2 S4 (Ruhezustand) D3 (komplett aus) S5 (komplett aus) D3 (komplett aus) Tabelle 6–14 Beispiel einer Zuordnungstabelle für System- und Energiegerätezustände Experiment: Die Energiezustandszuordnung eines Treibers einsehen Im Geräte-Manager können Sie die Zuordnungen zwischen System- und Geräteenergiezuständen für einen Treiber einsehen. Öffnen Sie dazu das Eigenschaftendialogfeld für ein Gerät, klicken Sie auf die Registerkarte Details und wählen Sie im Dropdownmenü Eigenschaft den Punkt Energiedaten. Das Dialogfeld zeigt auch den aktuellen Energiezustand des Geräts, die Fähigkeiten des Geräts zur Energieverwaltung (Energiekapazität) und die Energiezustände an, aus denen es das System aufwecken kann: Energieverwaltung Kapitel 6 685
Energieverwaltung durch die Treiber Wenn die Energieverwaltung einen Übergang zu einem anderen Systemenergiezustand durchführen will, sendet sie Energiebefehle an die Energie-Dispatchroutine eines Treibers (IRP_MJ_ POWER). Für die Verwaltung eines Geräts können mehrere Treiber zuständig sein, aber nur einer davon ist als Besitzer der Energierichtlinie für das Gerät vorgesehen. Gewöhnlich ist dies der Treiber, der das FDO verwaltet. Dieser Treiber bestimmt aufgrund des Systemstatus den Energiezustand des Geräts. Geht das System beispielsweise von S0 zu S3 über, kann der Treiber entscheiden, das Gerät von D0 aus in den Zustand D1 zu versetzen. Anstatt die anderen Treiber, die an der Verwaltung des Geräts beteiligt sind, direkt über diese Entscheidung zu informieren, bittet der Besitzer der Energierichtlinie für das Gerät die Energieverwaltung mithilfe der Funktion PoRequestPowerIrp, dies zu tun, indem sie einen Geräteenergiebefehl an die Energie-Dispatchroutinen dieser Treiber ausgibt. Dadurch hat die Energieverwaltung jederzeit die Kontrolle über die Anzahl der Energiebefehle, die auf dem System aktiv sind. Beispielsweise kann es sein, dass einige Geräte erheblich viel Strom zum Starten benötigen. Die Energieverwaltung sorgt dafür, dass solche Geräte nicht gleichzeitig hochgefahren werden. Zu vielen Energiebefehlen gibt es auch entsprechende Abfragebefehle. Wenn das System beispielsweise in einen Standbyzustand übergeht, fragt die Energieverwaltung die Geräte erst, ob dies akzeptabel ist. Ein Gerät, das gerade zeitkritische Operationen durchführt oder mit Gerätehardware kommuniziert, kann diesen Befehl ablehnen, sodass das System seinen aktuellen Energiezustand beibehält. Experiment: Die Systemfähigkeiten und die Systemrichtlinie zur Energieverwaltung einsehen Mit dem Kerneldebuggerbefehl !pocaps können Sie sich die Fähigkeiten eines Systems zur Energieverwaltung anschauen. Die folgende Ausgabe dieses Befehls stammt von einem x64-Laptop mit Windows 10: lkd> !pocaps PopCapabilities @ 0xfffff8035a98ce60 Misc Supported Features: PwrButton SlpButton Lid S3 S4 S5 HiberFile FullWake VideoDim Processor Features: Thermal Disk Features: Battery Features: BatteriesPresent Battery 0 - Capacity: 0 Granularity: 0 Battery 1 - Capacity: 0 Granularity: 0 Battery 2 - Capacity: 0 Granularity: 0 Wake Caps Ac OnLine Wake: Sx Soft Lid Wake: Sx → 686 Kapitel 6 Das E/A-System
RTC Wake: S4 Min Device Wake: Sx Default Wake: Sx Die Zeile Misc Supported Features gibt an, dass das System neben S0 (vollständig eingeschaltet) auch die Systemenergiezustände S3, S4 und S5 unterstützt (aber nicht S1 und S2) und über eine gültige Ruhezustandsdatei zur Speicherung des Systemarbeitsspeichers für den Zustand S4 verfügt. Auf der Seite Energieoptionen, die Sie erreichen, indem Sie in der Systemsteuerung auf Energieoptionen klicken, können Sie verschiedene Aspekte der Energierichtlinie des Systems einstellen. Welche Möglichkeiten Sie dabei haben, hängt von den Fähigkeiten des Systems zur Energieverwaltung ab. Originalgerätehersteller können Energieschemas hinzufügen. Um eine Liste der vorhandenen Schemas einzusehen, verwenden Sie den Befehl powercfg /list. C:\WINDOWS\system32>powercfg /list Bestehende Energieschemen (* Aktiv) ----------------------------------GUID des Energieschemas: 381b4222-f694-41f0-9685-ff5bb260df2e (Ausbalanciert) GUID des Energieschemas: 8759706d-706b-4c22-b2ec-f91e1ef6ed38 (HP Optimized (empfohlen)) * GUID des Energieschemas: 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c (Höchstleistung) GUID des Energieschemas: a1841308-3541-4fab-bc81-f71556f20b4a (Energiesparmodus) → Energieverwaltung Kapitel 6 687
Wenn Sie eine der vorgegebenen Planeinstellungen ändern, können Sie die Leerlaufgrenz­ werte festlegen, nach denen bestimmt wird, wann das System den Monitor und die Festplatten abschaltet und wann es in den Standbymodus (S3 im vorherigen Experiment) oder den Ruhezustand (S4) übergeht. Außerdem können Sie über den Link Energiesparplaneinstellungen ändern das Verhalten des Systems beim Drücken des Netz- und des Standbyschalters und beim Zuklappen des Laptops festlegen. Über den Link Erweiterte Energieeinstellungen ändern können Sie unmittelbar die Werte in der Energierichtlinie des Systems bearbeiten. Eine Anzeige dieser Werte ist auch mit dem Debuggerbefehl !popolicy möglich: lkd> !popolicy SYSTEM_POWER_POLICY (R.1) @ 0xfffff8035a98cc64 PowerButton: Sleep Flags: 00000000 Event: 00000000 SleepButton: Sleep Flags: 00000000 Event: 00000000 None Flags: 00000000 Event: 00000000 Sleep Flags: 00000000 Event: 00000000 OverThrottled: None Flags: 00000000 Event: 00000000 IdleTimeout: IdleSensitivity: 90% LidClose: Idle: 0 MinSleep: S3 MaxSleep: S3 LidOpenWake: S0 FastSleep: S3 1 WinLogonFlags: S4Timeout: 0 VideoTimeout: 600 VideoDim: 0 SpinTimeout: 4b0 OptForPower: 0 FanTolerance: 0% ForcedThrottle: 0% MinThrottle: 0% DyanmicThrottle: None (0) → 688 Kapitel 6 Das E/A-System
Die ersten Zeilen dieser Ausgaben beziehen sich auf das Verhalten bei der Betätigung der verschiedenen Schalter, wie es im Fenster Energieoptionen > Erweiterte Einstellungen festgelegt ist. Auf diesem System versetzen sowohl der Netz- als auch der Standbyschalter den Computer in einen Standbymodus. Beim Zuklappen dagegen passiert nichts. Die Timeoutwerte hinten in der Anzeige sind in Sekunden und in Hexadezimalschreibweise angegeben. Die Werte entsprechen unmittelbar denen, die im Fenster Energieoptionen eingestellt wurden. Beispielsweise beläuft sich der Videotimeoutwert auf 600, was bedeutet, dass der Monitor nach 600 s, also nach 10 Minuten abgeschaltet wird. (Aufgrund eines Bugs in den hier verwendeten Debuggingwerkzeugen wird dieser Wert im Dezimalformat angegeben.) Das Timeout zum Herunterfahren der Festplatte beträgt 0x4b0, also 1200 s oder 20 Minuten. Steuerung der Geräteenergiezustände durch den Treiber und die Anwendung Ein Treiber kann nicht nur auf Befehle der Energieverwaltung für den Wechsel zu einem anderen Systemenergiezustand reagieren, sondern den Energiezustand seiner Geräte auch von sich aus steuern. So kann es beispielsweise sein, dass der Treiber den Energieverbrauch eines Geräts verringern will, das längere Zeit inaktiv war. Das kann beispielsweise bei Monitoren und Festplatten geschehen, die ein Abdunkeln bzw. Herunterfahren zulassen. Der Treiber kann die Inaktivität eines Geräts entweder selbst oder unter Zuhilfenahme der Einrichtungen der Energieverwaltung erkennen. In letzterem Fall muss der Treiber das Gerät durch den Aufruf der Funktion PoRegisterDeviceForIdleDetection bei der Energieverwaltung registrieren. Diese Funktion teilt der Energieverwaltung die Timeoutwerte mit, die bestimmen, ob sich das Gerät im Leerlauf befindet, und gibt den Geräteenergiezustand an, den die Energieverwaltung anwenden soll. Der Treiber gibt zwei Timeouts an, nämlich einen für den Fall, dass der Benutzer den Computer zum Energiesparen konfiguriert hat, und einen zweiten für eine Konfiguration zur Optimierung der Leistung. Nach dem Aufruf von PoRegisterDeviceForIdleDetection muss der Treiber die Energieverwaltung durch einen Aufruf von PoSetDeviceBusy oder PoSetDeviceBusyEx immer informieren, wenn das Gerät aktiv ist, und sich dann erneut für die Leerlauferkennung registrieren, und sie nach Bedarf zu de- und zu reaktivieren. Es gibt auch die APIs PoStartDeviceBusy und PoEndDeviceBusy, mit denen sich das gleiche Verhalten unter Verwendung einer einfacheren Programmierlogik hervorrufen lässt. Ein Gerät hat zwar die Kontrolle über seinen eigenen Energiezustand, kann aber den Systemenergiezustand nicht beeinflussen, um Zustandsübergänge zu verhindern. Wenn beispielsweise ein schlecht geschriebener Treiber keinen Standbymodus erlaubt, kann er das Gerät nur eingeschaltet lassen oder komplett abschalten. Das ändert aber nichts an den Möglichkeiten des Systems, in einen Standbymodus zu wechseln, da die Energieverwaltung die Treiber nur über Zustandsübergänge informiert, aber nicht um deren Zustimmung bittet. Bevor das System in einen Zustand mit geringerem Energieverbrauch übergeht, erhalten die Treiber ein IRP mit einer Energieabfrage (IRP_MN_QUERY_POWER). Ein Treiber kann dagegen ein Veto einlegen, aber die Energieverwaltung muss dieses Veto nicht berücksichtigen. Sie kann den Übergang ver- Energieverwaltung Kapitel 6 689
zögern, falls das möglich ist (etwa wenn die Akkuladung noch keinen kritisch niedrigen Wert erreicht hat). Der Übergang in den Ruhezustand dagegen schlägt niemals fehl. Für die Energieverwaltung sind primär die Treiber und der Kernel verantwortlich, doch auch die Anwendungen können sich beteiligen. Benutzermodusprozesse können sich für verschiedene Energiebenachrichtigungen registrieren, z. B. für einen niedrigen oder kritischen Akkuladezustand, für den Wechsel des Computers von Akku- auf Netzbetrieb oder für die Auslösung eines Energiezustandsübergangs durch das System. Allerdings können Anwendungen kein Veto gegen solche Vorgänge einlegen. Vor dem Übergang in einen Standbymodus wird Ihnen ein Zeitraum von zwei Sekunden für alle erforderlichen Aufräumarbeiten gewährt. Das Framework für die Energieverwaltung Beginnend mit Windows 8 bietet der Kernel ein Framework für die Verwaltung der Energiezustände einzelner Komponenten eines Geräts (manchmal auch Funktionen des Geräts genannt). Beispielsweise kann ein Audiogerät Komponenten für die Wiedergabe und für die Aufnahme haben. Wenn die Wiedergabekomponente aktiv ist, die Aufnahmekomponente aber nicht, dann wäre es vorteilhaft, Letztere in einen Zustand mit geringerem Energieverbrauch zu versetzen. Das Power Management Framework (PoFx) stellt eine API bereit, mit deren Hilfe Treiber die Energiezustände und -erfordernisse ihrer Komponenten angeben können. Alle Komponenten müssen den Status »vollständig eingeschaltet« (F0) unterstützen. Höhere F-Nummern stehen für Zustände mit geringerem Energieverbrauch, wobei jeder F-Zustand weniger Energie verbraucht als der vorherige, aber dafür eine längere Übergangszeit für den Wechsel zurück zu F0 erfordert. Diese F-Zustände haben nur eine Bedeutung, wenn sich das Gerät im Zustand D0 befindet, da es in den D-Zuständen mit höheren Nummern gar nicht arbeitet. Der Besitzer der Energierichtlinie des Geräts (gewöhnlich das FDO) muss sich beim PoFx registrieren, indem er die Funktion PoFxRegisterDevice aufruft. Dabei übergibt der Treiber die folgenden Informationen: QQ Die Anzahl der Komponenten in dem Gerät. QQ Einen Satz von Callbacks, die der Treiber implementieren kann, um sich bei verschiedenen Ereignissen vom PoFx benachrichtigen zu lassen, beispielsweise beim Wechsel zu einem aktiven oder inaktiven Zustand, beim Übergang in den Zustand D0 und beim Senden von Energiesteuercodes. (Mehr darüber erfahren Sie im WDK.) QQ Für alle Komponenten jeweils die Anzahl der möglichen F-Zustände. QQ Für alle Komponenten jeweils den niedrigsten F-Zustand, aus dem sie aufwachen kann. QQ Für alle F-Zustände aller Komponenten die Zeit, die es erfordert, um zu F0 zurückzukehren; die Mindestzeit, die diese Komponenten in dem F-Zustand verbleiben müssen, damit der Übergang sich lohnt; und den Nennenergieverbrauch der Komponente in diesem Zustand. Es kann auch angegeben sein, dass der Energieverbrauch vernachlässigbar ist und es sich nicht lohnt, ihn zu berücksichtigen, wenn PoFx entscheidet, mehrere Komponenten gleichzeitig aufzuwecken. PoFx nutzt diese Informationen – sowie weitere Energieinformationen über das Gerät und das System, z. B. das aktuelle Energieprofil –, um eine fundierte Entscheidung zu treffen, in welchem 690 Kapitel 6 Das E/A-System
F-Zustand sich eine Komponente jeweils befinden soll. Die Kunst besteht dabei darin, zwischen zwei einander widersprechenden Zielen abzuwägen, nämlich dafür zu sorgen, dass eine inaktive Komponente so wenig Energie verbraucht wie möglich, aber zweitens auch sicherzustellen, dass eine Komponente schnell genug zurück in den Zustand F0 wechseln kann, sodass es so aussieht, als wäre die Komponente stets eingeschaltet und immer verbunden. Der Treiber muss PoFx informieren, wenn eine Komponente aktiv sein muss (F0), indem er PoFxActivateComponent aufruft. Danach ruft PoFx schließlich den zugehörigen Callback auf, um dem Treiber mitzuteilen, dass sich die Komponente jetzt im Zustand F0 befindet. Entscheidet der Treiber dagegen, dass die Komponente zurzeit nicht benötigt wird, ruft er PoFxIdleComponent auf, um dies PoFx mitzuteilen. Das Framework reagiert darauf, indem es die Komponente in einen F-Zustand mit geringerem Energieverbrauch versetzt und den Treiber nach getaner Arbeit da­ rüber informiert. Leistungszustandsverwaltung Der gerade beschriebene Mechanismus macht es möglich, dass eine Komponente im Leerlauf (in einem anderen Zustand als F0) weniger Energie verbraucht als in F0. Manche Komponenten aber können je nachdem, welche Arbeit das Gerät gerade verrichtet, selbst im Zustand F0 weniger Energie verbrauchen. Beispielsweise verbraucht eine Grafikkarte weniger Energie, wenn sie eine größtenteils statische Anzeige darstellt, als wenn sie eine 3-D-Animation mit 60 Bildern pro Sekunde rendert. In Windows 8.x mussten die entsprechenden Treiber einen eigenen Algorithmus zur Auswahl eines Leistungszustands implementieren und einen Betriebssystemdienst benachrichtigen, der als Plattformerweiterung-Plug-In (PEP) bezeichnet wurde, wobei ein PEP jeweils auf eine bestimmte Produktreihe von Prozessoren oder Ein-Chip-Systemen (System on Chip, SoC) zugeschnitten ist. Dadurch musste der Treibercode eng an das jeweilige PEP gekoppelt sein. In Windows 10 wurde die PoFx-API um eine Verwaltung von Leistungszuständen erweitert, sodass der Treibercode nun Standard-APIs verwenden kann und sich nicht mehr darum kümmern muss, welches PEP auf der jeweiligen Plattform eingesetzt wird. Für jede Komponente bietet PoFx die folgenden Arten von Leistungszuständen: QQ Diskrete Zustandsnummern, die für bestimmte Frequenzen (Hz) oder Bandbreiten (Bit pro Sekunde) stehen, oder Nummern, die nur für den Treiber eine Bedeutung haben QQ Kontinuierliche Zustandsverläufe zwischen einem Minimum und einem Maximum (Frequenz, Bandbreite, benutzerdefiniert) Beispielsweise kann für eine Grafikkarte ein Satz diskreter Frequenzen definiert werden, bei denen sie arbeiten kann, was sich indirekt auf ihren Energieverbrauch auswirkt. Ähnliche Leistungssätze können auch für die genutzte Bandbreite aufgestellt werden, wo dies sinnvoll ist. Um sich für die Verwaltung von Leistungszuständen bei PoFx zu registrieren, muss ein Treiber zunächst das Gerät wie im vorherigen Abschnitt registrieren (über PoFxRegisterDevice). Anschließend ruft er PoFxRegisterComponentPerfStates auf und übergibt dabei Angaben zur Leistung (diskret oder kontinuierlich, Frequenz, Bandbreite oder benutzerdefiniert) und einen Callback für den Fall, dass eine Statusänderung eintritt. Energieverwaltung Kapitel 6 691
Wenn ein Treiber entscheidet, dass eine Komponente ihren Leistungszustand ändern soll, ruft er PoFxIssuePerfStateChange oder PoFxIssuePerfStatechangeMultiple auf. Dadurch wird das PEP aufgefordert, die Komponente in den angegebenen Zustand zu versetzen (auf der Grundlage des angegebenen Index oder Wertes, je nachdem, ob der Satz für einen diskreten oder einen Bereichszustand gilt). Der Treiber kann auch angeben, ob der Aufruf synchron oder asynchron erfolgen soll oder ob es ihm egal ist; in letzterem Fall trifft das PEP die Entscheidung. In jedem Fall ruft PoFX schließlich den vom Treiber registrierten Callback mit dem Leistungszustand auf. Dabei kann es sich tatsächlich um den angeforderten Zustand handeln, es ist aber auch möglich, dass das PEP die Anforderung verweigert hat. Wurde die Anforderung akzeptiert, nimmt der Treiber die entsprechenden Aufrufe für seine Hardware vor, um den eigentlichen Wechsel vorzunehmen. Bei einer Ablehnung der Anforderung kann der Treiber es mit einem weiteren Aufruf einer der erwähnten Funktionen erneut versuchen. Der Callback des Treibers erfolgt jedoch schon nach einem einzigen Aufruf. Energieverfügbarkeitsanforderungen Gegen Übergänge in einen Standbymodus, die bereits ausgelöst wurden, können Anwendungen und Treiber kein Veto einlegen. In manchen Situationen ist allerdings ein Mechanismus nötig, um die Auslösung eines solchen Zustandsübergangs zu verhindern, wenn der Benutzer auf bestimmte Weise mit dem System arbeitet. Wenn sich der Benutzer beispielsweise einen Film ansieht und der Computer so eingestellt ist, dass er nach 15 Minuten ohne Maus- oder Tastaturbetätigungen in den Standbymodus übergeht, muss die Anwendung zur Medienwiedergabe die Möglichkeit haben, solche Übergänge auszuschließen, solange der Film läuft. Auch andere Energiesparmaßnahmen wie ein Abschalten oder auch nur eine Verdunkelung des Bildschirms würden die Freude an der Betrachtung des Films erheblich mindern. In älteren Windows-Ver­ sionen konnte die Benutzermodus-API SetThreadExecutionStation Zustandsübergänge des Systems und der Anwendung im Leerlauf unterbinden, indem sie die Energieverwaltung darüber informierte, dass der Benutzer den Computer nach wie vor nutzt. Allerdings bot diese API keinerlei Diagnosemöglichkeiten und auch keine ausreichend feinen Steuerungsmöglichkeiten, um die Verfügbarkeitsanforderung zu definieren. Des Weiteren konnten Treiber keine eigenen Anforderungen ausgeben. Benutzeranwendungen mussten ihr Threadmodell sogar entsprechend anpassen, da diese Anforderungen auf Thread-, und nicht auf Prozess- oder System­ ebene erfolgten. Mittlerweise jedoch unterstützt Windows Objekte für Verfügbarkeitsanforderungen, die im Kernel implementiert und im Objekt-Manager definiert sind. Mit dem Sysinternals-Tool WinObj (mehr darüber in Kapitel 8 von Band 2) können Sie sich den Objekttyp PowerRequest im Verzeichnis \ObjectTypes ansehen. Alternativ können Sie dazu auch den Kerneldebuggerbefehl !object auf den Objekttyp \ObjectTypes\PowerRequest anwenden. Energieverfügbarkeitsanforderungen werden von Benutzermodusanwendungen über die API PowerCreateRequest erstellt und mit PowerSetRequest und PowerClearRequest aktiviert bzw. deaktiviert. Im Kernel verwenden die Treiber dazu die Funktionen PoCreatePowerRequest, PoSetPowerRequest und PoClearPowerRequest. Da hier keine Handles verwendet werden, ist außerdem PoDeletePowerRequest erforderlich, um den Verweis auf das Objekt zu entfernen (wohingegen im Benutzermodus einfach CloseHandle aufgerufen werden kann). 692 Kapitel 6 Das E/A-System
Über die PowerRequest-API können vier Arten von Anforderungen eingesetzt werden: QQ Systemanforderung Bei dieser Art von Anforderung wird das System gebeten, bei Ablauf des Leerlauftimers nicht automatisch in einen Standbymodus zu wechseln (wobei der Benutzer jedoch nach wie vor die Möglichkeit hat, etwa den Laptop zuzuklappen, um einen Standbymodus hervorzurufen). QQ Anzeigeanforderung Diese Art von Anforderung macht dasselbe wie die Systemanforderung, allerdings für die Anzeige. QQ Abwesenheitsmodusanforderung Dies ist eine Änderung des normalen S3-Verhaltens von Windows, bei der der Computer vollständig eingeschaltet bleibt, Anzeige und Soundkarte aber ausgeschaltet sind. Für den Benutzer sieht es so aus, als befände sich der Computer tatsächlich in einem Standbymodus. Dieses Verhalten wird gewöhnlich nur von besonderen Set-Top-Boxen und Mediacentergeräten verwendet, bei denen die Medienauslieferung auch dann weitergehen muss, wenn der Benutzer etwa einen physischen Standbyschalter betätigt hat. QQ Anforderung für erforderliche Ausführung Mit dieser Art von Anforderung (die ab Windows 8 und Windows Server 2012 verfügbar ist) wird verlangt, den Prozess einer UWPApp auch dann fortzusetzen, wenn der Prozesslebenszyklus-Manager (PLM) ihn normalerweise (aus welchem Grunde auch immer) beendet hätte. Wie weit die Lebensdauer verlängert wird, hängt von verschiedenen Faktoren ab, darunter den Einstellungen der Energierichtlinie. Diese Art von Anforderung wird nur auf Systemen unterstützt, die den modernen Standbymodus bieten. Auf anderen Systemen wird sie als Systemanforderung interpretiert. Experiment: Energieverfügbarkeitsanforderungen einsehen Kernelobjekte für Energieanforderungen, die mit einem Aufruf wie PowerCreateRequest erstellt werden, sind in den öffentlichen Symbolen leider nicht zugänglich. Das Dienstprogramm powercfg bietet jedoch eine Möglichkeit, Energieanforderungen auch ohne Zuhilfenahme eines Kerneldebuggers anzuzeigen. Die folgende Ausgabe dieses Dienstprogramms stammt von einem Windows 10-Laptop, der gerade einen Video- und Audiostream aus dem Web wiedergab: c:\WINDOWS\system32>powercfg /requests DISPLAY: [PROCESS] \Device\HarddiskVolume4\Program Files\WindowsApps\Microsoft. ZuneVideo_10.16092.10311.0_x64__8wekyb3d8bbwe\Video.UI.exe Windows-Runtime-Paket: Microsoft.ZuneVideo_8wekyb3d8bbwe SYSTEM: [DRIVER] Conexant ISST Audio (INTELAUDIO\ FUNC_01&VEN_14F1&DEV_50F4&SUBSYS_103C80D3&R EV_1001\4&1a010da&0&0001) Ein Audiostream wird derzeit verwendet. → Energieverwaltung Kapitel 6 693
[PROCESS] \Device\HarddiskVolume4\Program Files\WindowsApps\Microsoft. ZuneVideo_10.16092.10311.0_x64__8wekyb3d8bbwe\Video.UI.exe Windows-Runtime-Paket: Microsoft.ZuneVideo_8wekyb3d8bbwe AWAYMODE: Keine. EXECUTION: Keine. PERFBOOST: Keine. ACTIVELOCKSCREEN: Keine. Diese Ausgabe zeigt sechs verschiedene Anforderungstypen statt der zuvor beschriebenen vier. Die beiden letzten – perfboost und activelockscreen – werden im Rahmen eines internen Energieanforderungstyps in einem Kernelheader deklariert, darüber hinaus aber zurzeit nicht verwendet. Zusammenfassung Das E/A-System definiert das Modell zur E/A-Verarbeitung auf Windows und führt allgemeine Funktionen sowie solche aus, die von mehr als einem Treiber benötigt werden. Seine Hauptaufgabe besteht darin, IRPs für E/A-Anforderungen zu erstellen, diese Pakete durch die verschiedenen Treiber zu lotsen und die Ergebnisse nach Abschluss der E/A-Operation an den Aufrufer zurückzugeben. Mithilfe von E/A-Systemobjekten, darunter Treiber- und Geräteobjekten, kann der E/A-Manager die einzelnen Treiber und Geräte ausfindig machen. Intern arbeitet das E/A-System von Windows asynchron, um eine hohe Leistung zu erzielen. Für Anwendungen im Benutzermodus stellt es Vorkehrungen sowohl für synchrone als auch für asynchrone E/A bereit. Zu den Gerätetreibern gehören nicht nur herkömmliche Hardwaregerätetreiber, sondern auch Dateisystem-, Netzwerk- und geschichtete Filtertreiber. Alle haben eine gemeinsame Struktur und kommunizieren über gemeinsame Mechanismen miteinander und mit dem E/A-Manager. Die Schnittstellen des E/A-Systems ermöglichen es, Treiber in einer Hochsprache zu schreiben, um die Entwicklungszeit zu verkürzen und die Portierbarkeit zu verbessern. Da Treiber für das Betriebssystem eine gleichartige Struktur aufweisen, können sie geschichtet werden, um Modularität zu erreichen und eine Duplizierung einzelner Aufgaben untereinander zu vermeiden. Durch die Verwendung von Universal-DDIs können Treiber ohne Änderungen am Code verschiedene Geräte und Bauformen ansprechen. 694 Kapitel 6 Das E/A-System
Der PnP-Manager arbeitet mit den Gerätetreibern zusammen, um Hardwaregeräte dynamisch zu erkennen und einen internen Gerätebaum für die Geräteauflistung und Treiberinstallation aufzustellen. Die Energieverwaltung arbeitet ebenfalls mit den Gerätetreibern zusammen, um Geräte in einen Zustand mit geringerem Energieverbrauch zu versetzen, wenn dies möglich ist, um Energie zu sparen und die Akkubetriebsdauer zu verlängern. Im nächsten Kapitel beschäftigen wir uns mit einem der zurzeit wichtigsten Aspekte von Computersystemen, nämlich der Sicherheit. Zusammenfassung Kapitel 6 695

K apite l 7 Sicherheit In jeder Umgebung, in der mehrere Benutzer Zugang zu denselben physischen Ressourcen oder Netzwerkressourcen haben, ist es von entscheidender Bedeutung, einen nicht autorisierten Zugriff auf sensible Daten zu verhindern. Das Betriebssystem und die einzelnen Benutzer müssen in der Lage sein, Dateien, den Arbeitsspeicher und Konfigurationseinstellungen vor unerwünschter Einsichtnahme und Änderung zu schützen. Die Betriebssystemsicherheit schließt auf der Hand liegende Mechanismen wie Konten, Kennwörter und Dateischutz ein, aber auch weniger offensichtliche Maßnahmen, wie das Betriebssystem vor Beschädigungen zu schützen, Benutzer mit geringeren Rechten von bestimmten Aktionen (wie etwa dem Neustart des Computers) abzuhalten und nicht zuzulassen, dass Programme eines Benutzers die Programme anderer Benutzer oder gar das Betriebssystem beeinträchtigen. In diesem Kapitel zeigen wir, wie sämtliche Aspekte des Designs und der Implementierung von Microsoft Windows auf die eine oder andere Weise durch strenge Anforderungen an eine solide Sicherheit beeinflusst wurden. Sicherheitseinstufungen Eine Einstufung von Software einschließlich Betriebssystemen anhand wohldefinierter Standards hilft Behörden, Unternehmen und Endbenutzern, auf Computern gespeicherte firmen­ eigene und persönliche Daten zu schützen. In den Vereinigten Staaten und in vielen anderen Ländern bilden die Common Criteria (CC) zurzeit den gültigen Standard für die Sicherheitseinstufung. Um die in Windows eingebauten Sicherheitsvorkehrungen zu verstehen, ist es jedoch sinnvoll, sich auch mit der Geschichte des Systems für Sicherheitseinstufungen zu beschäftigen, das das Design von Windows beeinflusst hat: Trusted Computer System Evaluation Criteria (TCSEC). Trusted Computer System Evaluation Criteria Das National Computer Security Center (NCSC) wurde 1981 als Teil der National Security Agency (NSA) des US-Verteidigungsministeriums eingerichtet. Ein Ziel des NCSC bestand darin, die in Tabelle 7–1 aufgeführten Sicherheitseinstufungen aufzustellen, um den Grad des Schutzes bestimmen zu können, den kommerzielle Betriebssysteme, Netzwerkkomponenten und vertrauenswürdige Anwendungen anbieten. Diese Sicherheitsstufen wurden 1983 definiert und als Orange Book bezeichnet. Sie finden sie auf http://csrc.nist.gov/publications/history/dod85.pdf. 697
Einstufung Beschreibung A1 Verifiziertes Design B3 Sicherheitsbereiche B2 Strukturierter Schutz B1 Schutz durch Sicherheitskennzeichen C2 Schutz durch gesteuerten Zugriff C1 Schutz durch diskreten Zugriff (veraltet) D Minimaler Schutz Tabelle 7–1 TCSEC-Einstufungen Die Einstufungen des TCSEC-Standards stellen unterschiedliche Grade an Vertrauenswürdigkeit dar, wobei die höheren Ebenen durch Ergänzung um rigorosere Schutz- und Validierungsanforderungen aus den unteren hervorgehen. Es gibt kein Betriebssystem, das die Voraussetzungen für die A1-Einstufung (verifiziertes Design) erfüllt. Einige haben zwar B-Einstufungen erhalten, doch C2 wird als ausreichend angesehen und ist die höchste Einstufung, die für ein Allzweck-Betriebssystem praktisch geeignet ist. Die folgenden Schlüsselanforderungen für die Sicherheitseinstufung C2 gelten nach wie vor als wichtigste Erfordernisse für sichere Betriebssysteme: QQ Eine Einrichtung für die sichere Anmeldung Dazu ist es erforderlich, dass die Benutzer eindeutig identifiziert werden können und nur dann Zugriff auf den Computer erhalten, wenn sie sich auf irgendeine Weise authentifiziert haben. QQ Zugriffssteuerung im Ermessen des Besitzers Der Besitzer einer Ressource (etwa einer Datei) kann festlegen, welche Benutzer auf die Ressource zugreifen können und was sie damit anstellen dürfen. Er vergibt Rechte für verschiedene Arten des Zugriffs an Benutzer oder Benutzergruppen. QQ Sicherheitsüberwachung Dies bezeichnet die Fähigkeit, sicherheitsrelevante Ereignisse sowie jegliche Vorgänge zu erkennen und zu melden, bei denen versucht wird, Systemressourcen zu erstellen, anzufassen oder zu löschen. Die Identitäten aller Benutzer werden mit Anmeldekennungen festgehalten, sodass sich leicht feststellen lässt, wenn jemand eine nicht autorisierte Aktion durchführt. QQ Schutz vor Objektwiederverwendung Dadurch wird verhindert, dass Benutzer Daten sehen, die ein anderer Benutzer gelöscht hat, oder auf Arbeitsspeicher zugreifen, den ein anderer Benutzer zuvor verwendet und dann freigegeben hat. Beispielsweise ist es in manchen Betriebssystemen möglich, eine neue Datei einer bestimmten Länge zu erstellen und dann ihren Inhalt zu untersuchen, um sich anzusehen, welche Daten sich zuvor an dem Speicherort auf der Festplatte befanden, dem die Datei zugewiesen wurde. Diese Daten können aber sensible Informationen aus einer inzwischen gelöschten Datei eines anderen Benutzers umfassen. Der Schutz vor Objektwiederverwendung stopft diese Sicherheitslücke, indem er sämtliche Objekte, also auch Dateien und Arbeitsspeicher, vor der Zuweisung zu einem Benutzer initialisiert. 698 Kapitel 7 Sicherheit
Windows erfüllt auch zwei Anforderungen für die Sicherheitsstufe B: QQ Vertrauenswürdige Pfade Dadurch wird verhindert, dass trojanische Pferde Benutzernamen und Kennwörter bei der Anmeldung abfangen. In Windows wird diese Maßnahme durch die Anmeldetastenfolge (Strg) + (Alt) + (Entf) umgesetzt, die von nicht privilegierten Anwendungen nicht abgefangen werden kann. Diese Tastenfolge, die auch als Sicherheitsaufruf (Secure Attention Sequence, SAS) bezeichnet wird, zeigt immer einen systemgesteuerten Windows-Sicherheitsbildschirm (falls der Benutzer bereits angemeldet ist) oder den Anmeldebildschirm an, sodass trojanische Pferde leicht zu erkennen sind. (Die SAS kann auch von einem Programm aus über die API SendSAS gesendet werden, wenn die Gruppenrichtlinie und andere Einschränkungen das zulassen.) Ein trojanisches Pferd, das ein gefälschtes Anmeldedialogfeld anzeigt, wird bei der Angabe der SAS umgangen. QQ Verwaltung durch vertrauenswürdige Einrichtungen Hierzu muss es separate Kontorollen für unterschiedliche administrative Funktionen geben, also beispielsweise eigene Konten für Administratoren, für Benutzer, die auf dem Computer Sicherungen durchführen, und für Standardbenutzer. Durch sein Sicherheitsteilsystem und die damit zusammenhängenden Komponenten erfüllt Windows alle diese Kriterien. Common Criteria Im Januar 1996 haben die USA, Großbritannien, Deutschland, Frankreich, Kanada und die Niederlande die gemeinsam entwickelte Spezifikation Common Criteria for Information Technology Security Evaluation (CCITSE) herausgegeben, üblicherweise kurz Common Criteria (CC) genannt. Diese Kriterien bilden den anerkannten internationalen Standard zur Bewertung der Produktsicherheit. Die Website für CC finden Sie auf http://www.niap-ccevs.org/cc-scheme. Die CC sind flexibler als die TCSEC-Einstufungen und weisen eine Struktur auf, die sich stärker am ITSEC- als am TCSEC-Standard orientiert. In den CC gibt es den Begriff des Schutzprofils (Protection Profile, PP), mit dem Sicherheitsanforderungen zu leicht spezifizierbaren und vergleichbaren Sätzen gebündelt werden, und der Sicherheitsvorgabe (Security Target, ST), die über einen Verweis auf ein PP einen Satz von anwendbaren Sicherheitsanforderungen enthält. Außerdem definieren die CC sieben Stufen für die Vertrauenswürdigkeit der Zertifizierung, die als »Evaluation Assurance Levels« (ELAs) bezeichnet werden. Dadurch entfernen die CC (ebenso wie zuvor der ITSEC-Standard) die Verbindung zwischen Funktionalität und Vertrauenswürdigkeit, die es in TCSEC und früheren Zertifizierungsverfahren gab. Windows 2000, Windows XP, Windows Server 2003 und Windows Vista Enterprise haben eine Common-Criteria-Zertifizierung mit dem Schutzprofil für gesteuerten Zugriff (Controlled Access Protection Profile, CAPP) erreicht, was ungefähr der TCSEC-Einstufung C2 entspricht. Alle erhielten die Bewertung EAL 4+, wobei das Plus für »Mängelsanierung« steht. EAL 4 ist der höchste Grad, der international anerkannt wird. Im März 2011 wurden Windows 7 und Windows Server 2008 R2 nach den Anforderungen von Version 1 des Schutzprofils der US-Regierung für Allzweck-Betriebssysteme in Netzwerk­ umgebungen überprüft (US Government Protection Profile for General-Purpose Operating Sicherheitseinstufungen Kapitel 7 699
S­ ystems in a Networked Environment, GPOSPP; siehe http://www.commoncriteriaportal.org/­ files/ppfiles/pp_gpospp_v1.0.pdf). Diese Zertifizierung schließt auch den Hypervisor Hyper-V ein. Abermals erzielte Windows EAL 4 mit Mängelsanierung (EAL 4+). Der Validierungsbericht ist auf http://www.commoncriteriaportal.org/files/epfiles/st_vid10390-vr.pdf zu finden, und die Beschreibung der Sicherheitsvorgänge mit Angaben zu den einzelnen erfüllten Voraussetzungen auf http://www.commoncriteriaportal.org/files/epfiles/st_vid10390-st.pdf. Ähnliche Zertifizierungen erhielten Windows 10 und Windows Server 2012 R2 im Juni 2016. Die Berichte hierzu stehen auf http://www.commoncriteriaportal.org/files/epfiles/cr_windows10.pdf zur Verfügung. Systemkomponenten für die Sicherheit Die folgenden Kernkomponenten und Datenbanken implementieren die Sicherheitsvorkehrungen von Windows. Sofern nicht anders angegeben, befinden sich alle genannten Dateien im Verzeichnis %SystemRoot%\System32. QQ Security Reference Monitor, SRM Diese Komponente der Windows-Exekutive (Ntoskrnl.exe) ist dafür zuständig, die Datenstruktur für ein Zugriffstoken zur Darstellung eines Sicherheitskontexts zu definieren, Zugriffsprüfungen an Objekten durchzuführen, Benutzerrechte zu bearbeiten und jegliche aus diesen Tätigkeiten resultierenden Meldungen für die Sicherheitsüberwachung zu generieren. QQ Teilsystemdienst für die lokale Sicherheitsautorität (Local Security Authority Subsystem Service, Lsass) Dieser Benutzermodusprozess wird im Abbild Lsass.exe ausgeführt und kümmert sich um die lokale Systemsicherheitsrichtlinie (die u. a. festlegt, welche Benutzer sich an dem Computer anmelden dürfen, und Kennwortrichtlinien, Rechte für Benutzer und Gruppen und die Einstellungen für die Systemsicherheitsüberwachung umfasst), die Benutzerauthentifizierung und darum, Meldungen der Sicherheitsüberwachung an das Ereignisprotokoll zu senden. Der Großteil dieser Funktionalität ist im Dienst der lokalen Sicherheitsautorität (Lsasrv.dll) implementiert, einer Bibliothek, die Lsass lädt. QQ LSAIso.exe Diese Komponente wird auch als Credential Guard (siehe den gleichnamigen Abschnitt weiter hinten) bezeichnet. Lsass verwendet sie, um Hashes von Benutzertokens zu speichern, anstatt sie im eigenen Arbeitsspeicher aufzubewahren. Da es sich bei Lsaiso. exe um ein Trustlet handelt – einen IUM-Prozess (Isolated User Mode) –, das auf VTL1 ausgeführt wird, kann kein normaler Prozess auf seinen Adressraum zugreifen, nicht einmal der normale Kernel. Lsass selbst speichert ein verschlüsseltes BLOB des Kennworthashs, der für die Kommunikation mit Lsaiso (über ALPC) benötigt wird. QQ Lsass-Richtliniendatenbank Diese Datenbank enthält die Einstellungen der lokalen Systemsicherheitsrichtlinie und ist in der Registrierung in einem durch eine Zugriffssteuerungsliste geschützten Bereich unter HKLM\SECURITY gespeichert. Sie gibt beispielsweise an, welchen Domänen die Authentifizierung von Anmeldeversuchen anvertraut wird, wer die Berechtigung hat, in welcher Weise auf das System zuzugreifen (interaktiv, über das Netzwerk und über Dienstanmeldungen), wem welche Rechte zugewiesen sind und welche Form von Sicherheitsüberwachung ausgeführt wird. Außerdem sind in der Lsass-Richtliniendatenbank »Geheimnisse« wie die Anmeldeinformationen für zwischengespeicherte 700 Kapitel 7 Sicherheit
Domänenanmeldungen und Benutzerkontoanmeldungen von Windows-Diensten abgelegt. (Weitere Informationen über Windows-Dienste erhalten Sie in Kapitel 9 von Band 2.) QQ Sicherheitskonto-Manager (Security Accounts Manager, SAM) Dieser Dienst (in älteren deutschen Versionen »Sicherheitskontoverwaltung« genannt) verwaltet die Datenbank mit den auf dem lokalen Computer definierten Benutzernamen und Gruppen. Der in Samsrv.dll implementierte SAM-Dienst wird in den Lsass-Prozess geladen. QQ SAM-Datenbank Diese Datenbank enthält die definierten lokalen Benutzer und Gruppen mit ihren Kennwörtern und sonstigen Attributen. Auf Domänencontrollern speichert der Sicherheitskonto-Manager nicht die in der Domäne definierten Benutzer, sondern die Definition und das Kennwort für das Wiederherstellungskonto des Systemadministrators. Diese Datenbank befindet sich in der Registrierung unter HKLM\SAM. QQ Active Directory Dieser Verzeichnisdienst enthält eine Datenbank mit Informationen über die Objekte in einer Domäne wie Benutzer, Gruppen und Computer. In diesem Zusammenhang ist eine Domäne eine Menge von Computern und ihrer zugehörigen Sicherheitsgruppen, die als eine Einheit verwaltet werden. In Active Directory sind auch die Kennwortinformationen und Rechte für Domänenbenutzer und Gruppen gespeichert und werden auf die Computer repliziert, die als Domänencontroller für die Domäne eingerichtet sind. Der Active Directory-Server ist in Ntdsa.dll implementiert und wird im Lsass-­ Prozess ausgeführt. Weitere Informationen über Active Directory erhalten Sie in Kapitel 10 von Band 2. QQ Authentifizierungspakete Dazu gehörigen die DLLs (Dynamic Link Libraries), die im Kontext sowohl des Lsass-Prozesses als auch der Clientprozesse ausgeführt werden und die Authentifizierungsrichtlinie von Windows implementieren. Authentifizierungs-DLLs authentifizieren Benutzer, indem sie prüfen, ob der angegebene Benutzername und das Kennwort übereinstimmen (oder je nach dem verwendeten Mechanismus zur Angabe von Anmeldeinformationen entsprechend vorgehen). Wenn ja, geben sie Informationen über die Sicherheitsidentität des Benutzers an Lsass zurück, woraufhin Lsass diese Informationen wiederum nutzt, um ein Token zu erstellen. QQ Manager für die interaktive Anmeldung (Winlogon) Dieser Benutzermodusprozess führt Winlogon.exe aus, das dafür zuständig ist, auf die SAS zu reagieren und interaktive Anmeldesitzungen zu verwalten. Unter anderem erstellt Winlogon bei der Anmeldung eines Benutzers dessen ersten Prozess. QQ Benutzerschnittstelle für die Anmeldung (LogonUI) Dieser Benutzermodusprozess führt das Abbild LogonUI.exe aus, das den Benutzern die Oberfläche anzeigt, an der sie sich gegenüber dem System authentifizieren können. Dabei nutzt LogonUI Anmeldeinformationsanbieter, um die verschiedenen Arten von Anmeldeinformationen von Benutzern abzufragen. QQ Anmeldeinformationsanbieter (Credential Providers, CPs) Hierbei handelt es sich um prozessinterne COM-Objekte, die im Prozess LogonUI laufen (der wiederum bei Bedarf von Winlogon gestartet wird, wenn die SAS-Tastenfolge betätigt wird) und dazu dienen, den Benutzernamen und das Kennwort, die Smartcard-PIN, biometrische Daten (z. B. Fingerabdrücke) oder andere Identifizierungsmerkmale zu erfassen. Die Standard-Anmelde­ Systemkomponenten für die Sicherheit Kapitel 7 701
informationsanbieter sind Authui.dll, SmartcardCredentialProvider.dll, BioCredProv.dll und FaceCredentialProvider.dll, wobei der Anbieter für die Gesichtserkennung in Windows 10 hinzugefügt wurde. QQ Netzwerkanmeldedienst (Netlogon) Dieser Windows-Dienst (Netlogon.dll, ausgeführt in einem regulären Svchost-Hostprozess) richtet den sicheren Kanal zu einem Domänencontroller ein, über den Sicherheitsanforderungen gesendet werden, z. B. interaktive Anmeldungen (wenn der Domänencontroller Windows NT 4 ausführt) oder eine Authentifizierungsvalidierung durch LAN Manager oder NT LAN Manager (v1 und v2). Netlogon wird auch für die Active Directory-Anmeldung verwendet. QQ Kernel Security Device Driver (KSecDD) Die Funktionen in dieser Kernelmodusbibliothek (%SystemRoot%\System32\Drivers\Ksecdd.sys) implementierten die Schnittstellen für erweiterte lokale Prozeduraufrufe (Advanced Local Procedure Calls, ALPCs), die andere Sicherheitskomponenten im Kernelmodus – etwa das verschlüsselnde Dateisystem (Encrypting File System, EFS) – zur Kommunikation mit Lsass im Benutzermodus verwenden, QQ AppLocker Dieser Mechanismus ermöglicht Administratoren, festzulegen, welche ausführbaren Dateien, DLLs und Skripte die angegebenen Benutzer und Gruppen verwenden dürfen. AppLocker besteht aus einem Treiber (%SystemRoot%\System32\Drivers\Appld.sys) und einem Dienst (AppldSvc.dll), die in einem standardmäßigen SvcHost-Prozess ausgeführt werden. Abb. 7–1 zeigt die Beziehungen zwischen einigen dieser Komponenten und den von ihnen verwalteten Datenbanken. Ereignis­proto­ kollierung Netlogon Active Directory LSA-Server SAM-Server LSA-Richtlinie Benutzermodus Kernelmodus Exekutive Abbildung 7–1 Sicherheitskomponenten von Windows 702 Kapitel 7 Sicherheit
Experiment: HKLM\SAM und HKLM\Security einsehen Die mit den Registrierungsschlüsseln SAM und Security verknüpften Sicherheitsdeskriptoren verhindern, dass irgendein Konto außer dem lokalen Systemkonto auf sie zugreifen kann. Eine Möglichkeit, um sich diese Schlüssel trotzdem anzusehen, besteht darin, ihre Sicherheitseinstellungen zurückzusetzen, was aber die Sicherheit des Systems schwächt. Eine andere Vorgehensweise besteht darin, Regedit.exe unter dem lokalen Systemkonto auszuführen. Das können Sie wie folgt mit dem Sysinternals-Tool PsExec und der Option -s tun: C:\>psexec –s –i –d c:\windows\regedit.exe Der Schalter -i weist PsExec an, die angegebene ausführbare Datei in einer interaktiven Fensterstation auszuführen. Ohne diese Angabe würde der Prozess in einer nicht interaktiven Fensterstation laufen, also auf einem unsichtbaren Desktop. Der Schalter -d gibt an, dass PsExec nicht auf die Beendigung des Zielprozesses warten soll. Der SRM im Kernelmodus und Lsass im Benutzermodus kommunizieren über die in Kapitel 8 von Band 2 beschriebenen ALPCs. Während der Initialisierung des Systems erstellt SRM den Port SeRmCommandPort, mit dem Lsass Verbindung aufnimmt, und wenn der Lsass-Prozess startet, legt er den ALPC-Port SeLsaCommandPort an, mit dem wiederum der SRM Verbindung aufnimmt. Dadurch entstehen private Kommunikationsports. Der SRM erstellt einen gemeinsamen Speicherbereich für Nachrichten, die länger als 256 Bytes sind, und übergibt im Verbindungsaufruf ein Handle. Nachdem der SRM und Lsass bei der Systeminitialisierung miteinander Verbindung aufgenommen haben, lauschen sie nicht mehr auf ihren jeweiligen Verbindungsports. Dadurch haben Benutzerprozesse keine Möglichkeit mehr, Verbindung mit einem dieser Ports aufzunehmen, um sie für böse Zwecke zu missbrauchen. Eine solche Verbindungsanforderung wird niemals abgeschlossen. Systemkomponenten für die Sicherheit Kapitel 7 703
Virtualisierungsgestützte Sicherheit Da der Kernel von Natur aus höhere Rechte erfordert und von Benutzermodusanwendungen getrennt ist, wird er gemeinhin auch als vertrauenswürdig bezeichnet. Es werden jedoch jeden Monat zahllose Drittanbietertreiber geschrieben. Laut Aussage von Microsoft sind in den Telemetriedaten jeden Monat eine Million verschiedene Treiberhashes zu erkennen. Jeder dieser Treiber kann Schwachstellen enthalten – ganz zu schweigen von bewusst eingebautem Kernelmodus-Schadcode. Angesichts dieser Tatsachen ist die Vorstellung, dass der Kernel eine kleine, geschützte Komponente ist und Benutzermodusanwendungen vor Angriffen aus dem Kernel sicher sind, offensichtlich unrealistisch. Es ist nicht angebracht, dem Kernel voll zu vertrauen. Wichtige Benutzermodusanwendungen, die sehr private Benutzerdaten enthalten können, sind Angriffen sowohl durch schädliche Benutzermodusanwendungen (die fehlerhafte Kernelmoduskomponenten ausnutzen) als auch durch schädliche Kernelmodusprogramme ausgesetzt. Wie bereits in Kapitel 2 erwähnt, weisen Windows 10 und Windows Server 2016 eine Architektur für virtualisierungsgestützte Sicherheit (Virtualization-Based Security, VBS) auf, die eine zusätzliche, orthogonale Vertrauensebene ins Spiel bringt, nämlich die virtuelle Vertrauensebene (Virtual Trust Level, VTL). In diesem Abschnitt erfahren Sie, wie Credential Guard und Device Guard VTLs nutzen, um Benutzerdaten zu schützen und eine zusätzliche Sicherheitsschicht auf der Grundlage vertrauenswürdiger Hardware für die digitale Codesignierung bereitzustellen. Am Endes dieses Kapitels werden Sie außerdem sehen, wie Kernel Patch Protection (KPP) durch die Komponente PatchGuard bereitgestellt und von der VBS-gestützten HyperGuard-Technologie noch verstärkt wird. Zur Auffrischung: Normaler Benutzer- und Kernelmoduscode läuft auf VTL 0 und weiß nichts von VTL 1. Alles, was auf VTL 1 läuft, ist daher für VTL-0-Code unsichtbar und nicht zugänglich. Kann Malware in den normalen Kernel vordringen, ist es ihr trotzdem nicht möglich, Zugriff auf irgendetwas zu bekommen, das auf VTL 1 gespeichert ist, nicht einmal auf Benutzermoduscode, der auf dieser Ebene läuft (was als isolierter Benutzermodus oder Isolated User Mode bezeichnet wird). Abb. 7–2 zeigt die wichtigsten VBS-Komponenten, die wir uns in diesem Abschnitt ansehen werden: QQ Hypervisor-Based Code Integrity (HVCI) und Kernel-Mode Code Integrity (KMCI), die Mechanismen hinter Device Guard QQ LSA (Lsass.exe) und isolierte LSA (Lsaiso.exe), die Mechanismen hinter Credential Guard Schlagen Sie auch noch einmal die Implementierung von Trustlets, die in IUM laufen, in Kapitel 3 nach. 704 Kapitel 7 Sicherheit
Kernel Mode Code Integrity Isolierte LSA Apps Windows-Plattformdienste HVCI Kernel Virtueller sicherer Modus (VSM) Kernel Windows (Hostbetriebssystem) Abbildung 7–2 VBS-Komponenten Wie jede andere vertrauenswürdige Komponente setzt auch VTL 1 voraus, dass die Komponenten, von denen sie abhängt, vertrauenswürdig sind. Daher ist es für VTL 1 erforderlich, dass der sichere Start (und damit die Firmware) korrekt funktioniert, der Hypervisor nicht beschädigt wurde und Hardwareelemente wie IOMMU und die Intel Management Engine keine von VTL 0 aus zugänglichen Schwachstellen aufweisen. Weitere Informationen über die Hardware-Vertrauenskette und Sicherheitstechnologien für den Start erhalten Sie in Kapitel 11 von Band 2. Credential Guard Um zu verstehen, welche Sicherheitsschranken und Schutzmaßnahmen Credential Guard bietet, müssen wir uns zunächst mit den einzelnen Komponenten befassen, die den Zugriff auf die Ressourcen und Daten eines Benutzers und die Anmeldeeinrichtungen in einer Netzwerkumgebung gewähren: QQ Kennwort Dies ist die primäre Art von Anmeldeinformation, mit der sich Benutzer interaktiv am Computer anmelden. Sie wird zur Authentifizierung und zur Ableitung der anderen Komponenten des Anmeldeinformationsmodells genutzt und bildet den entscheidenden Aspekt der Identität eines Benutzers. QQ NT-Einwegfunktion (NT One-Way Function, OWF) Dies ist ein Hash, der von veralteten Komponenten unter Verwendung des Protokolls NT LAN Manager (NTLM) zur Identifizierung des Benutzers verwendet wird (nach erfolgreicher Anmeldung mit einem Kennwort). Moderne Netzwerksysteme setzen zur Benutzerauthentifizierung kein NTLM mehr ein, doch bei vielen lokalen Komponenten und auch bei einigen Arten veralteter Netzwerkkomponenten (wie NTLM-gestützten Authentifizierungsproxies) ist das nach wie vor der Fall. Die NTOWF ist jedoch nur ein MD4-Hash. Aufgrund des mangelnden Schutzes gegen Wiedereinspielen und der für moderne Hardware geringen algorithmischen Kom- Virtualisierungsgestützte Sicherheit Kapitel 7 705
plexität dieses Hashs führt ein Abfangen der NTOWF zu einer unmittelbaren Gefährdung und unter Umständen sogar zu einer Ermittlung des Kennworts. QQ Ticketgewährendes Ticket (TGT) Dies ist das Gegenstück zur NTOWF bei der Verwendung des weit moderneren Remoteauthentifizierungsmechanismus Kerberos. In Active Directory-Domänen ist Kerberos die Standardvorgehensweise, in Windows Server 2016 wird die Verwendung von Kerberos sogar erzwungen. Das TGT und der zugehörige Schlüssel werden dem lokalen Computer nach einer erfolgreichen Anmeldung bereitgestellt (wie die NTOWF bei NTLM). Werden beide Komponenten abgefangen, so sind die Anmeldeinformationen des Benutzers damit unmittelbar gefährdet. Wiedereinspielung und Ermittlung des Kennworts sind jedoch nicht möglich. Wenn Credential Guard nicht aktiviert ist, sind einige oder alle dieser Bestandteile der Anmeldeinformationen eines Benutzers im Arbeitsspeicher von Lsass vorhanden. Um Credential Guard in den Editionen Windows 10 Enterprises und Windows Server 2016 zu aktivieren, öffnen Sie den Gruppenrichtlinien-Editor (gpedit.msc) und wählen Computerkonfiguration > Administrative Vorlagen > System > Device ­Guard und dann Virtualisierungsbasierte Sicherheit aktivieren. Klicken Sie im oberen linken Bereich des daraufhin eingeblendeten Dialogfelds auf Aktiviert und wählen Sie schließlich eine der Aktiviert-Optionen im Kombinationsfeld Credential Guard-Konfiguration aus. Kennwortschutz Das Kennwort wird mit einem lokalen symmetrischen Schlüssel verschlüsselt und gespeichert, um eine einmalige Anmeldung (Single Sign-On, SSO) über Protokolle wie Digestauthentifizierung (WDigest, was seit Windows XP für die HTTP-Authentifizierung verwendet wird) oder Terminaldienste/RDP zu ermöglichen. Da diese Protokolle eine Klartextauthentifizierung durchführen, ist es erforderlich, diese Kennwörter im Arbeitsspeicher zu halten, der aber durch Codeinjektion, Debugger und andere Exploittechniken zugänglich ist, und zu entschlüsseln. Credential Guard kann diese von Natur aus unsicheren Protokolle nicht ändern. Als einzige Lösung bleibt Credential Guard daher nur übrig, die SSO-Funktionalität für solche Protokolle zu deaktivieren. Das führt jedoch zu einem Verlust an Kompatibilität und zwingt den Benutzer zu einer erneuten Authentifizierung. Eine bessere Lösung besteht offensichtlich darin, ganz auf die Verwendung von Kennwörtern zu verzichten. Mit Windows Hello, das in einem eigenen Abschnitt weiter hinten beschrieben wird, ist das möglich. Bei einer Authentifizierung über biometrische Verfahren wie Gesichts- oder Fingerabdruckerkennung ist es nicht mehr nötig, ein Kennwort einzugeben und die Anmeldeinformationen vor Hardware-Keyloggern, Kernelsniffern und Kernelhooking-Werkzeugen und gefälschten Benutzermodusanwendungen zu schützen. Wenn Benutzer niemals ein Kennwort eingeben müssen, kann auch kein Kennwort mehr gestohlen werden. Eine andere Form sicherer Anmeldeinformationen bildet die Kombination aus einer Smartcard und der zugehörigen PIN. Die PIN kann zwar bei der Eingabe erfasst werden, aber die Smartcard ist ein physisches Element, das sich ohne einen komplizierten hardwaregestützten Angriff 706 Kapitel 7 Sicherheit
nicht abfangen lässt. Dies ist eine Form der Zwei-Faktor-Authentifizierung, die es auch noch in vielen anderen Varianten gibt. Schutz der NTOWF bzw. des TGT-Schlüssels Auch wenn die interaktiven Anmeldeinformationen geschützt sind, führt eine erfolgreiche Anmeldung dazu, dass das Schlüsselverteilungscenter (Key Distribution Center, KDC) des Domänencontrollers das TGT und seinen Schlüssel sowie für ältere Anwendungen auch die NTOWF zurückgibt. Der Benutzer verwendet die NTOWF für den Zugriff auf ältere Ressourcen und das TGT und dessen Schlüssel, um ein Dienstticket zu erstellen, das dann für den Zugriff auf Remote­ressourcen (wie Dateien auf einer Freigabe) eingesetzt werden kann (siehe Abb. 7–3). rer ifizie t n e th sel 1. Au hlüs + Sc T G T T 2. TG ssel chlü S + ket sttic Dien 3. Die nsttic ket Dateie n Datei­ server Abbildung 7–3 Zugriff auf Remoteressourcen Wenn ein Angreifer aber die NTOWF oder das TGT und dessen Schlüssel (gespeichert in Lsass) in die Hände bekommt, kann er damit auch ohne Smartcard, PIN, das Gesicht oder die Fingerabdrücke des Benutzers auf Ressourcen zugreifen. Eine denkbare Sicherheitsmaßnahme besteht darin, Lsass vor Zugriff durch Angreifer zu schützen, was mit der in Kapitel 3 beschriebenen PPL-Architektur (Protected Process Light) möglich ist. Um dafür zu sorgen, dass Lsass geschützt ausgeführt wird, setzen Sie den DWORD-Wert RunAsPPL im Registrierungsschlüssel HKLM\System\CurrentControlSet\Control\Lsa auf 1. (Dies ist nicht die Standardoption, da auch legitime Authentifizierungsanbieter von Drittanbietern [DLLs] im Kontext von Lsass geladen und ausgeführt werden, was nicht möglich ist, wenn Lsass im geschützten Modus läuft.) Durch diese Maßnahme werden zwar die NTOWF und der TGT-Schlüssel vor Angriffen aus dem Benutzermodus geschützt, aber weder vor Angriffen aus dem Kernel noch vor Benutzermodusangriffen, die eine Schwachstelle in einem der Millionen pro Monat produzierten Treiber ausnutzen. Credential Guard löst dieses Problem durch den Prozess Lsaiso.exe. Er wird als Trustlet auf VTL 1 ausgeführt und speichert die Geheimnisse des Benutzers daher in seinem Arbeitsspeicher und nicht in Lsass. Virtualisierungsgestützte Sicherheit Kapitel 7 707
Sichere Kommunikation Wie bereits in Kapitel 2 erklärt, weist die Ebene VTL 1 eine geringe Angriffsfläche auf, da sie weder über einen vollständigen regulären NT-Kernel noch über Treiber oder E/A-Zugriff auf irgendwelche Hardware verfügt. Daher kann die isolierte LSA, bei der es sich um ein VTL-1Trustlet handelt, nicht direkt mit dem KDC kommunizieren. Dies liegt nach wie vor im Aufgabenbereich des Lsass-Prozesses, der als Proxy und Protokollimplementierer fungiert. Er kommuniziert mit dem KDC, um den Benutzer zu authentifizieren, das TGT, den Schlüssel und die NTOWF in Empfang zu nehmen, und kommuniziert mithilfe des Diensttickets mit dem Dateiserver. Hier scheint jedoch ein Problem vorzuliegen: Das TGT und sein Schlüssel bzw. die NTOWF durchlaufen während der Authentifizierung Lsass, und das TGT und sein Schlüssel stehen Lsass auf irgendeine Weise zur Verfügung, um die Diensttickets generieren zu können. Das führt zu den beiden folgenden Fragen: Wie kann Lsass die Geheimnisse in der isolierten LSA senden und empfangen, und wie können wir einen Angreifer daran hindern, dies ebenfalls zu tun? Um die erste Frage zu beantworten, denken Sie daran, welche Dienste laut Kapitel 3 für Trustlets zur Verfügung stehen. Einer davon ist Advanced Local Procedure Call (ALPC), den der sichere Kernel dadurch unterstützt, dass er NtAlpc*-Aufrufe als Proxy an den normalen Kernel weiterleitet. Außerdem implementiert der isolierte Benutzermodus (Isolated User Mode, IUM) eine Unterstützung für die RPC-Laufzeitbibliothek (Rpcrt4.dll) über das ALPC-Protokoll, wodurch VLT-0- und VLT-1-Anwendungen wie alle anderen Anwendungen und Dienste über lokale RPCs kommunizieren können. Abb. 7–4 zeigt einen Screenshot von Process Explorer. Hier sehen Sie den Prozess Lsaiso.exe mit einem ALPC-Handle für den Port LSA_ISO_RPC_SERVER, der für die Kommunikation mit dem Prozess Lsass.exe verwendet wird. (Mehr über ALPC erfahren Sie in Kapitel 8 von Band 2.) Zur Beantwortung der zweiten Frage sind gewisse Kenntnisse über kryptografische Protokolle und Challenge/Response-Modelle erforderlich. Wenn Sie bereits mit den Grundprinzipien der SSL/TLS-Technologie und ihrer Anwendung zur Verhinderung von Man-in-the-Middle-Angriffen bei der Kommunikation im Internet vertraut sind, können Sie sich das Protokoll für das KDC und die isolierte LSA ähnlich vorstellen. Lsass befindet sich zwar wie ein Proxy in der Mitte, sieht aber nur verschlüsselten Datenverkehr zwischen dem KDC und der isolierten LSA, ohne die übertragenen Inhalte verstehen zu können. Die isolierte LSA richtet einen lokalen Sitzungsschlüssel ein, der nur auf VTL 1 gültig ist, verschlüsselt ihn mit einem weiteren Schlüssel, der nur dem KDC bekannt ist, und sendet ihn über ein sicheres Protokoll. Das KDC antwortet dann mit dem TGT und dessen Schlüssel, die er mit dem Sitzungsschlüssel der isolierten LSA verschlüsselt. Daher sieht Lsass nur eine verschlüsselte Nachricht an das KDC und eine verschlüsselte Nachricht vom KDC, die er beide nicht entschlüsseln kann. 708 Kapitel 7 Sicherheit
Abbildung 7–4  LsaIso.exe und sein ALPC-Port Mit diesem Modell lässt sich auch die veraltete NTLM-Authentifizierung schützen, die auf dem Challenge/Response-Modell beruht. Wenn sich beispielsweise ein Benutzer mit Klartext-Anmeldeinformationen anmeldet, sendet LSA diese an die isolierte LSA, die sie mit ihrem Sitzungsschlüssel verschlüsselt und in dieser verschlüsselten Form an Lsass zurückgibt. Wird später ein Challenge/Response-Vorgang für NTLM benötigt, sendet Lsass die NTLM-Challenge und die verschlüsselten Anmeldeinformationen an die isolierte LSA. Zu diesem Zeitpunkt verfügt nur die isolierte LSA über den Verschlüsselungsschlüssel, sodass sie die Anmeldeinformationen entschlüsseln und die passende NTLM-Antwort generieren kann. Beachten Sie jedoch, dass bei diesem Modell vier Arten von Angriffen möglich sind: QQ Ist der Computer bereits physisch übernommen, kann das Klartextkennwort bei der Eingabe oder beim Senden an die isolierte LSA abgefangen werden (wenn Lsass übernommen wurde). Die Verwendung von Windows Hello kann dieses Risiko senken. QQ Wie bereits erwähnt verfügt NTLM nicht über einen Wiedereinspielschutz. Wird die NTLM-Antwort abgefangen, kann sie daher später für die gleiche Challenge wieder eingespielt werden. Kann ein Angreifer nach der Anmeldung Lsass knacken, so kann er darüber die verschlüsselten Anmeldeinformationen abfangen und die isolierte LSA zwingen, neue NTLM-Antworten für beliebige NTLM-Challenges zu generieren. Dies funktioniert aber nur bis zu einem Neustart, da die isolierte LSA danach einen neuen Sitzungsschlüssel erstellt. QQ Bei der Kerberos-Anmeldung kann die NTOWF (die nicht verschlüsselt ist) wie bei einem normalen Pass-the-Hash-Angriff abgefangen und wiederverwendet werden. Auch dazu ist jedoch ein bereits geknackter Computer (oder eine physische Möglichkeit zum Abfangen der Informationen im Netzwerk) erforderlich. QQ Ein Benutzer mit physischem Zugang zu dem Computer kann Credential Guard abschalten (»Downgrade-Angriff«). Danach wird das ältere Authentifizierungsmodell verwendet, weshalb sich auch ältere Angriffsmethoden wieder einsetzen lassen. Virtualisierungsgestützte Sicherheit Kapitel 7 709
UEFI-Sperre Angreifer können Credential Guard auf einfache Weise ausschalten, da es sich dabei im Grunde genommen lediglich um eine Registrierungseinstellung handelt. Als Gegenmaßnahme können Secure Boot und UEFI eingesetzt werden, um nicht physisch anwesende Administratoren (wie Malware mit Administratorrechten) daran zu hindern. Dazu wird Credential Guard mit UEFI-Sperre aktiviert. In diesem Modus wird eine EFI-Laufzeitvariable in den Firmware-Arbeitsspeicher geschrieben, woraufhin ein Neustart erforderlich ist. Dabei schreibt der Windows-Bootloader, der nach wie vor im EFI-Startdienstmodus läuft, eine EFI-Bootvariable (die nach dem Verlassen des EFI-Startdienstmodus weder les- noch schreibbar ist), um darin die Tatsache festzuhalten, dass Credential Guard aktiviert ist. Außerdem wird eine Option in der Startkonfigurationsdatenbank (Boot Configuration Database, BCD) aufgezeichnet. Wenn der Kernel startet und diese BCD-Option oder die UEFI-Laufzeitvariable vorhanden ist, schreibt er den erforderlichen Registrierungsschlüssel für Credential Guard neu. Wird die BCD-Option von einem Angreifer gelöscht, erkennen BitLocker (falls aktiviert) und der TPM-­ gestützte Remotenachweis (falls aktiviert) die Änderung, woraufhin sie die physische Eingabe des Wiederherstellungsschlüssels des Administrators verlangen und einen Neustart ausführen. Dabei wird die BCD-Option auf der Grundlage der UEFI-Laufzeitvariablen wiederhergestellt. Wird die UEFI-Laufzeitvariable gelöscht, so stellt der Windows-Bootloader sie auf der Grundlage der UEFI-Startvariablen wieder her. Ohne besonderen Code zum Löschen der UEFI-Bootvariablen – was nur im EFI-Startdienstmodus geschehen kann – gibt es daher keine Möglichkeit, Credential Guard im UEFI-Sperrmodus auszuschalten. Der einzige existierende Code dieser Art befindet sich in der besonderen Microsoft-Binärdatei SecComp.efi. Sie muss vom Administrator herunterladen werden, und anschließend muss der Administrator den Computer von einem anderen EFI-Gerät aus starten und die Datei manuell ausführen (wozu sowohl der BitLocker-Wiederherstellungsschlüssel als auch physischer Zugriff erforderlich sind) oder die BDC bearbeiten (wozu der BitLocker-Wiederherstellungsschlüssel erforderlich ist). Beim Neustart verlangt SecComp.efi eine Benutzerbestätigung im UEFI-Modus (was nur durch einen physisch anwesenden Benutzer erfolgen kann). Authentifizierungsrichtlinien und Kerberos-Armoring Das Sicherheitsmodell »sicher, sofern der Computer nichts bereits vor der Anmeldung geknackt wurde oder ein Angriff durch einen physisch anwesenden Administrator erfolgt« ist sicherlich besser als das herkömmliche Modell ohne Credential Guard. Manche Unternehmen und Organisationen benötigen jedoch eine noch stärkere Sicherheitsgarantie: Selbst ein geknackter Computer darf nicht dazu verwendet werden können, Anmeldeinformationen eines Benutzers zu fälschen oder wieder einzuspielen, und selbst wenn die Anmeldeinformationen eines Benutzers gestohlen wurden, dürfen sie außerhalb bestimmter Systeme nicht genutzt werden können. Durch die Nutzung von Authentifizierungsrichtlinien – einem Merkmal von Windows Server 2016 – und Kerberos-Armoring kann Credential Guard auch in diesem verstärkten Sicherheitsmodus ausgeführt werden. 710 Kapitel 7 Sicherheit
Dabei erfasst der sichere VLT-1-Kernel mithilfe des TPMs (es kann auch eine Datei auf der Festplatte verwendet werden, aber das würde die Sicherheit hinfällig machen) einen besonderen Schlüssel für die Computer-ID. Mit diesem Schlüssel wird dann ein TGT-Schlüssel für den Computer generiert, wenn der Computer beim Beitritt zu einer Domäne bereitgestellt wird. (Bei dieser Bereitstellung ist es natürlich wichtig, sich zu vergewissern, dass sich der Computer in einem vertrauenswürdigen Zustand befindet.) Dieser TGT-Schlüssel wiederum wird an das KDC gesendet. Wenn sich ein Benutzer anmeldet, werden seine Anmeldeinformationen mit denen des Computers (auf die nur die isolierte LSA Zugriff hat) zu einem Ursprungsnachweisschlüssel kombiniert. Bei dieser Vorgehensweise sind zwei Dinge garantiert: QQ Der Benutzer authentifiziert sich an einem bekannten Computer Wenn ein Benutzer oder ein Angreifer versucht, sich mit den richtigen Benutzeranmeldeinformationen von einem anderen Computer aus anzumelden, weichen seine Computeranmeldeinformationen ab, da sie auf dem TPM beruhen. QQ Die NTLM-Antwort bzw. das Benutzerticket kommt von der isolierten LSA und wurde nicht manuell von Lsass generiert Damit ist garantiert, dass Credential Guard auf dem Computer aktiviert ist, selbst wenn ein physisch anwesender Benutzer es deaktivieren kann. Auch hier gilt jedoch leider wieder: Wenn der Computer schon so weit übernommen wurde, dass die mit dem Ursprungsnachweis verschlüsselte KDC-Antwort mit dem Benutzer-TGT und dem zugehörigen Schlüssel abgefangen werden kann, ist es möglich, sie zu speichern und dazu zu nutzen, von der isolierten LSA Diensttickets anzufordern, die mit dem Sitzungsschlüssel verschlüsselt sind. Diese Diensttickets können dann beispielsweise an einen Dateiserver gesendet werden, um darauf zuzugreifen, bis der Sitzungsschlüssel bei einem Neustart gelöscht wird. Für Systeme mit Credential Guard wird daher empfohlen, jedes Mal einen Neustart auszuführen, wenn sich ein Benutzer abmeldet, da ein Angreifer sonst auch dann noch in der Lage ist, gültige Tickets auszugeben, wenn der Benutzer nicht mehr anwesend ist. Zukünftige Verbesserungen Wie bereits in Kapitel 2 und 3 erwähnt, wird der sichere Kernel auf VTL 1 zurzeit Verbesserungen unterzogen, um auch Sonderklassen für PCI- und USB-Hardware zu unterstützen, mit denen eine Kommunikation ausschließlich über den Hypervisor und VTL-1-Code mit Secure Device Framework (SDF) möglich ist. Zusammen mit den neuen Trustlets BioIso.exe und FsIso.exe zur sicheren Erfassung von biometrischen Daten und Videoframes (von einer Webcam) sorgt dies dafür, dass Komponenten im VTL-0-Kernelmodus die Inhalte von Windows Hello-Authentifizierungsversuchen nicht abfangen können (die wir im Gegensatz zu Klartext-Benutzerkennwörtern als sicher einstufen, obwohl sie technisch durch maßgeschneiderte, treibergestützte Abfangaktionen erfasst werden können). Es ist auf Hardwareebene garantiert, dass Windows Hello-Anmeldeinformationen niemals von VTL 0 aus zugänglich sind. In diesem Modus ist eine Beteiligung von Lsass an der Windows Hello-Authentifizierung nicht erforderlich. Die isolierte LSA ruft die Anmeldeinformationen unmittelbar von dem isolierten Biometrie- oder Video­ framedienst ab. Virtualisierungsgestützte Sicherheit Kapitel 7 711
Secure Driver Framework (SDF) ist das Gegenstück zu WDF für VTL-1-Treiber. Dieses Framework ist zurzeit noch nicht öffentlich zugänglich, steht aber Microsoft-Partnern zur Entwicklung von VTL-1-Treibern zur Verfügung. Device Guard Credential Guard dient zum Schutz der Anmeldeinformationen von Benutzern, Device G ­ uard dagegen zum Schutz des Computers selbst vor verschiedenen Arten von Software- und Hardwareangriffen. Device Guard nutzt die Windows-Codeintegritätsdienste wie Kernelmodus-Codesignierung (KMCS) und Benutzermodus-Codeintegrität (UMCI) und verstärkt sie durch Hypervisor-Codeintegrität (HVCI). Mehr über Codeintegrität erfahren Sie in Kapitel 8 von Band 2. Außerdem kann Device Guard vollständig konfiguriert werden. Dies geschieht mithilfe von Custom Code Integrity (CCI) und Signierrichtlinien, die von Secure Boot geschützt und vom Unternehmensadministrator definiert werden. Diese Richtlinien, die in Kapitel 8 erklärt werden, ermöglichen es, Einschluss- und Ausschlusslisten durchzusetzen, die auf der Grundlage kryptografisch sinnvoller Informationen (wie dem Unterzeichner eines Zertifikats oder einem SHA-2-Hash) und nicht wie bei AppLocker-Richtlinien aufgrund von Dateipfaden oder -namen aufgestellt werden. (Weitere Informationen über AppLocker erhalten Sie in dem gleichnamigen Abschnitt weiter hinten in diesem Kapitel.) Wir werden hier nicht die verschiedenen Möglichkeiten beschreiben, um Codeintegritätsrichtlinien aufzustellen und anzupassen, sondern erklären, wie Device Guard diese Richtlinien durchsetzt. Dabei wird Folgendes garantiert: QQ Wird die Codesignierung im Kernelmodus durchgesetzt, kann nur signierter Code geladen werden, selbst wenn der Kernel übernommen wurde Der Grund dafür ist, dass der Kernelladeprozess den sicheren Kernel in VLT 1 informiert, wenn er einen Treiber lädt, und den Ladevorgang nur dann erfolgreich abschließen kann, wenn HVCI die Signatur validiert hat. QQ Wird die Codesignierung im Kernelmodus durchgesetzt, kann signierter Code nach dem Laden nicht mehr geändert werden, nicht einmal vom Kernel selbst Der Grund dafür ist, dass die ausführbaren Codeseiten durch den SLAT-Mechanismus (Second Level Address Translation) des Hypervisors als schreibgeschützt gekennzeichnet werden. Dies wird ausführlicher in Kapitel 8 von Band 2 erklärt. QQ Wird die Codesignierung im Kernelmodus durchgesetzt, ist dynamisch zugewiesener Code unzulässig (was aus den ersten beiden Punkten hervorgeht) Der Grund dafür ist, dass der Kernel keine Möglichkeit hat, in SLAT-Tabelleneinträgen ausführbare Einträge zuzuweisen, selbst wenn die Seitentabellen des Kernels den Code als ausführbar kennzeichnen. QQ Wird die Codesignierung im Kernelmodus durchgesetzt, kann UEFI-Laufzeitcode nicht geändert werden, auch nicht von anderem UEFI-Laufzeitcode und dem Kernel selbst Außerdem sollte Secure Boot bereits überprüft haben, dass dieser Code zum 712 Kapitel 7 Sicherheit
L­ adezeitpunkt signiert war. (Device Guard stützt sich auf diese Annahme.) Außerdem können UEFI-Laufzeitdaten nicht ausführbar gemacht werden. Die Überprüfung erfolgt dadurch, dass der gesamte UEFI-Laufzeitcode und die UEFI-Laufzeitdaten gelesen und die korrekten Berechtigungen durchgesetzt und in die von VTL 1 geschützten SLAT-Seitentabelleneinträge dupliziert werden. QQ Wird die Codesignierung im Kernelmodus durchgesetzt, kann nur signierter Kernelmoduscode (Ring 0) ausgeführt werden Diese Aussage scheint aus den ersten drei Punkten hervorzugehen. Betrachten Sie aber den Fall von signiertem Ring-3-Code. Für UMCI ist er gültig, und er wird auch in den SLAT-Seitentabelleneinträgen als ausführbarer Code autorisiert. Der sichere Kernel stützt sich auf ein als MBEC bezeichnetes Hardwaremerkmal (Mode-Based Execution Control), das die SLAT um ein Ausführbarkeitsbit für Benutzer- und Kernelmodus erweitert, oder auf die Softwareemulation dieses Merkmals im Hypervisor, die eingeschränkter Benutzermodus (Restricted User Mode, RUM) genannt wird. QQ Wird die Codesignierung im Benutzermodus durchgesetzt, können nur signierte Benutzermodusabbilder geladen werden Das bedeutet, dass sowohl die ausführbaren Prozesse (.exe-Dateien) als auch die Bibliotheken, die sie laden (.dll), signiert sein müssen. QQ Wird die Codesignierung im Benutzermodus durchgesetzt, erlaubt der Kernel den Benutzermodusanwendungen nicht, vorhandene ausführbare Codeseiten schreibbar zu machen Benutzermodus ist offensichtlich nicht in der Lage, ausführbaren Arbeitsspeicher zuzuweisen oder vorhandenen Arbeitsspeicher zu ändern, ohne den Kernel um Erlaubnis zu bitten. Daher kann der Kernel seine üblichen Regeln anwenden. Doch selbst wenn der Kernel übernommen wurde, sorgt die SLAT nach wie vor dafür, dass Benutzermodusseiten ohne Wissen und Genehmigung des sicheren Kernels nicht ausführbar gemacht werden können und dass ausführbare Seiten niemals schreibbar werden. QQ Wird die Codesignierung im Benutzermodus durchgesetzt und fordert die Signierrichtlinie harte Codegarantien an, ist eine dynamische Zuweisung von Code unzulässig Dies ist ein wichtiger Unterschied zu den Situationen mit Kernelcodesignierung. Signierter Benutzermoduscode darf standardmäßig zusätzlichen ausführbaren Arbeitsspeicher zuweisen, um JIT-Vorgänge zu unterstützen, es sei denn, dass das Zertifikat der Anwendung eine besondere erweiterte Schlüsselverwendung (Enhanced Key Usage, EKU) enthält, die angibt, ob dynamische Codegenerierung zulässig ist. Zurzeit verfügt NGen.exe (.NET Native Image Generation) über diese EKU, die es ausführbaren .NET-Dateien in reiner IL ermöglicht, sogar in diesem Modus zu funktionieren. QQ Wird der eingeschränkte PowerShell-Sprachmodus durchgesetzt, müssen alle PowerShell-Skripte signiert werden, die dynamische Typen, Reflexion und andere Sprachmerkmale einsetzen, mit denen eine Ausführung, willkürlicher Code oder ein Marshalling zu Windows- oder .NET-API-Funktionen erlaubt wird Dadurch wird verhindert, dass möglicherweise schädliche PowerShell-Skripte aus dem eingeschränkten Modus ausbrechen. SLAT-Seitentabelleneinträge sind in VTL 1 geschützt und enthalten die grundlegenden Angaben darüber, welche Berechtigungen eine gegebene Seite im Arbeitsspeicher haben kann. Virtualisierungsgestützte Sicherheit Kapitel 7 713
Durch Zurückhalten des Ausführbarkeitsbits nach Bedarf und/oder Zurückhalten des Schreibbarkeitsbits von vorhandenen ausführbaren Seiten (ein Sicherheitsmodell, das als W^X, also W XOR X bezeichnet wird), verschiebt Device Guard die Durchsetzung der Codesignierung komplett nach VTL 1 (in eine Bibliothek namens Skci.dll, was für Secure Kernel Code Integrity steht). Selbst wenn Device Guard auf einem Computer nicht ausdrücklich eingerichtet ist, läuft es in einem dritten Modus, wenn Credential Guard aktiviert ist. Dabei erzwingt es, dass alle Trustlets über eine bestimmte Microsoft-Signatur mit einem Zertifikat verfügen, das die IUMEKU enthält. Wäre das nicht der Fall, könnte ein Widersacher mit Ring-0-Rechten den regulären KMCS-Mechanismus angreifen und ein schädliches Trustlet laden, um die isolierte LSA zu attackieren. Darüber hinaus werden alle Aspekte der Benutzermodus-Codesignierung für das Trustlet durchgesetzt, sodass es im Modus mit harten Codegarantien ausgeführt wird. Um die Leistung zu optimieren, authentifiziert der HVCI-Mechanismus nicht jede einzelne Seite neu, wenn das System aus dem Ruhezustand (S4) aufwacht. Es kann sogar sein, dass die Zertifikatdaten nicht einmal verfügbar sind. In einem solchen Fall müssen die SLAT-Daten rekonstruiert werden, was bedeutet, dass die SLAT-Seitentabelleneinträge in der Ruhezustandsdatei gespeichert werden müssen. Daher muss der Hypervisor darauf vertrauen können, dass die Ruhezustandsdatei nicht manipuliert wurde. Dazu wird die Ruhezustandsdatei mit dem lokalen Computerschlüssel verschlüsselt, der im TPM gespeichert ist. Ist kein TPM vorhanden, muss der Schlüssel leider in einer UEFI-Laufzeitvariablen abgelegt werden, was es einem lokalen Angreifer ermöglicht, die Ruhezustandsdatei zu entschlüsseln, zu ändern und wieder zu verschlüsseln. Objekte schützen Objektschutz und Zugriffsprotokollierung sind das Herz der besitzerverwalteten Zugriffssteuerung und der Überwachung. Zu den Objekten, die in Windows geschützt werden können, gehören Dateien, Geräte, Mailslots, Pipes (benannte und unbenannte), Jobs, Prozesse, Threads, Ereignisse, mit Schlüsseln versehene Ereignisse, Ereignispaare, Mutexe, Semaphoren, gemeinsam genutzte Speicherabschnitte, E/A-Vervollständigungsports, LPC-Ports, Timer mit Wartemöglichkeit, Zugriffstokens, Volumes, Fensterstationen, Desktops, Netzwerkfreigaben, Dienste, Registrierungsschlüssel, Drucker, Active Directory-Objekte usw. – theoretisch alles, was durch den Objekt-Manager der Exekutive verwaltet wird. In der Praxis werden Objekte, die nicht im Benutzermodus zugänglich gemacht werden (z. B. Treiberobjekte), üblicherweise nicht geschützt. Kernelmoduscode gilt als vertrauenswürdig und verwendet gewöhnlich Schnittstellen zum Objekt-Manager, bei denen keine Zugriffsprüfung erfolgt. Da Systemressourcen, die in den Benutzermodus exportiert werden (und daher eine Sicherheitsvalidierung erfordern), als Objekte im Kernelmodus implementiert sind, spielt der Objekt-Manager von Windows eine wichtige Rolle für die Objektsicherheit. Den Objektschutz (von benannten Objekten) können Sie sich mit dem Sysinternals-Tool WinObj ansehen (siehe Abb. 7–5). Abb. 7–6 zeigt die Registerkarte Sicherheit des Eigenschaftendialogfelds für ein Abschnittsobjekt in der Benutzersitzung. Dateien sind die Ressourcen, die man am häufigsten mit dem Objektschutz in Verbindung bringt, aber Windows verwendet auch für Exekutivobjekte dasselbe Sicherheitsmodell und dieselben Mechanismen wie für die 714 Kapitel 7 Sicherheit
Dateien im Dateisystem. Was die Zugriffssteuerung angeht, unterscheiden sich Exekutivobjekte nur durch die jeweils unterstützten Zugriffsmethoden von Dateien. Abbildung 7–5 Ein Abschnittsobjekt in WinObj Abbildung 7–6 Ein Exekutivobjekt und sein Sicherheitsdeskriptor in WinObj Objekte schützen Kapitel 7 715
Was Sie in Abb. 7–6 sehen, ist im Grunde genommen die besitzerverwaltete Zugriffssteuerungsliste (Discretionary Access Control List, DACL) des Objekts. Im Abschnitt »Sicherheitsdeskriptoren und Zugriffssteuerung« werden wir uns ausführlicher mit DACLs beschäftigen. In Process Explorer können Sie sich die Sicherheitseigenschaften von Objekten ansehen, indem Sie auf ein Handle im unteren Bereich doppelklicken, sofern Sie Process Explorer zur Anzeige von Handles eingerichtet haben, was den zusätzlichen Vorteil bietet, dass auch unbenannte Objekte ausgegeben werden. Die angezeigte Eigenschaftenseite ist in beiden Werkzeugen identisch, da die Seite selbst von Windows bereitgestellt wird. Um festzulegen, wer ein Objekt bearbeiten darf, muss sich das Sicherheitssystem zunächst über die Identität der einzelnen Benutzer vergewisasern. Aus diesem Grund verlangt Windows vor dem Zugriff auf jegliche Systemressourcen eine authentifizierte Anmeldung. Wenn ein Prozess ein Handle für ein Objekt anfordert, bestimmen der Objekt-Manager und das Sicherheitssystem anhand der Sicherheitskennung des Aufrufers und des Sicherheitsdeskriptors für das Objekt, ob dem Aufrufer das Handle übergeben werden soll, das dem Prozess Zugriff auf das gewünschte Objekt gewährt. Wie weiter hinten in diesem Kapitel ausführlich beschrieben wird, kann ein Thread einen anderen Sicherheitskontext als den seines Prozesses annehmen, was als Identitätswechsel bezeichnet wird. Führt ein Thread einen Identitätswechsel durch, so verwenden die Mechanismen zur Sicherheitsvalidierung den Sicherheitskontext des Threads, anderenfalls den seines Prozesses. Denken Sie daran, dass alle Threads in einem Prozess dieselbe Handletabelle nutzen. Wenn ein Thread ein Objekt öffnet, haben alle anderen Threads in seinem Prozess daher ebenfalls Zugriff auf dieses Objekt, selbst wenn der Thread einen Identitätswechsel durchgeführt hat. Manchmal reicht die Validierung der Identität eines Benutzers nicht aus, um Zugriff auf eine Ressource zu gewähren, die nur für ein bestimmtes Konto zugänglich sein soll. Beispielsweise besteht ein großer Unterschied zwischen einem Dienst, der unter dem Konto Alice läuft, und einer Anwendung, die Alice aus dem Internet heruntergeladen hat. Diese Art von Benutzerseparierung wird durch den Windows-Integritätsmodus erreicht, der Integritätsebenen einrichtet. Dieser Mechanismus wird von der Benutzerkontensteuerung, von der Benutzeroberflächen-Rechteisolierung (User Interface Privilege Isolation, UIPI) sowie von Anwendungscon­ tainern genutzt, die in diesem Kapitel alle noch beschrieben werden. Zugriffsprüfungen Das Windows-Sicherheitsmodell verlangt, dass ein Thread schon beim Öffnen eines Objekts angibt, welche Aktionen er daran ausführen möchte. Der Objekt-Manager ruft den SRM auf, um diesen gewünschten Zugriff zu prüfen. Wird der Zugriff gewährt, so wird dem Prozess des Threads ein Handle zugewiesen, mit dem der Thread (sowie andere Threads in dem Prozess) weitere Operationen an dem Objekt ausführen können. Wenn ein Thread ein vorhandenes Objekt anhand seines Namens öffnet, führt der Objekt-Manager eine Zugriffsvalidierung durch. Dabei schlägt er das angegebene Objekt zunächst in seinem Namensraum nach. Ist es nicht in einem der sekundären Namensräume vorhanden, etwa im Registrierungsnamensraum des Konfigurations-Managers oder im Dateisystemnamensraum des Dateisystemtreibers, ruft der Objekt-Manager die interne Funktion ObpCreateHandle auf, sobald er das Objekt gefunden hat. Wie der Name schon sagt, 716 Kapitel 7 Sicherheit
erstellt ObpCreateHandle in der Handletabelle des Prozesses einen Eintrag, der mit dem Objekt verknüpft wird. ObpCreateHandle ruft zunächst ObpGrantAccess auf, um zu sehen, ob der Thread die Berechtigung hat, auf das Objekt zuzugreifen. Wenn ja, ruft ObpCreateHandle als Nächstes die Exekutivfunktion ExCreateHandle auf, um einen Eintrag in der Handletabelle des Prozesses anzulegen. Um die Zugriffsprüfung durchzuführen, ruft ObpGrantAccess die Funk­ tion ObCheckObjectAccess auf. ObpGrantAccess übergibt die Sicherheitsanmeldeinformationen des Threads, die gewünschte Art des Zugriffs auf das Objekt (Lesen, Schreiben, Löschen usw. oder objektspezifische Operationen) und einen Zeiger auf das Objekt an ObCheckObjectAccess. Als Erstes sperrt diese Funktion den Sicherheitsdeskriptor des Objekts und den Sicherheitskontext des Threads, um zu verhindern, dass die Sicherheitseinstellungen des Objekts bzw. die Sicherheitsidentität des Threads während der Zugriffsprüfung von einem anderen Thread in dem System geändert werden. Anschließend ruft ObCheckObjectAccess die Sicherheitsmethode des Objekts auf, um dessen Sicherheitseinstellungen abzurufen. (Objektmethoden werden in Kapitel 8 von Band 2 erläutert.) Durch den Aufruf der Sicherheitsmethode kann eine Funktion in einer anderen Exekutivkomponente aufgerufen werden. Viele Exekutivobjekte greifen jedoch auf die standardmäßige Unterstützung des Systems für die Sicherheitsverwaltung zurück. Wenn eine Komponente der Exekutive ein Objekt definiert und die SRM-Standardsicherheitsrichtlinie nicht überschreiben möchte, kennzeichnet sie das Objekt als eines mit Standardsicherheit. Beim Aufruf der Sicherheitsmethode eines Objekts prüft der SRM als Erstes, ob das Objekt über Standardsicherheit verfügt. Bei solchen Objekten werden die Sicherheitsinformationen im Header gespeichert und als Sicherheitsmethode SeDefaultObjectMethod verwendet. Objekte, die sich nicht auf die Standardsicherheit verlassen, müssen ihre Sicherheitsinformationen selbst verwalten und eine eigene Sicherheitsmethode bereitstellen. Zu den Objekten mit Standardsicherheit zählen Mutexe, Ereignisse und Semaphoren. Bei Dateiobjekten kann die Standardsicherheit überschrieben werden. Der Objekt-Manager, der den Dateiobjekttyp festlegt, überlässt es dem zuständigen Dateisystemtreiber, die Sicherheit für seine Dateien auszuwählen (oder auf ihre Implementierung zu verzichten). Wenn das System die Sicherheitsinformationen eines Dateiobjekts für eine Datei in einem NTFS-Volume abfragt, wendet sich die Sicherheitsmethode für das Dateiobjekt daher an den NTFS-Dateisystemtreiber. Beachten Sie aber, dass ObCheckObjectAccess nicht ausgeführt wird, wenn Dateien geöffnet werden, da sie sich dann in sekundären Namensräumen befinden. Das System ruft die Sicherheitsmethode eines Dateiobjekts nur dann auf, wenn ein Thread ausdrücklich die Sicherheitsinformationen einer Datei abfragt oder festlegt (z. B. mit den Windows-Funktionen GetFileSecurity bzw. SetFileSecurity). Nach dem Abruf der Sicherheitsinformationen eines Objekts ruft ObCheckObjectAccess die SRM-Funktion SeAccessCheck ab, die zum Kern des Windows-Sicherheitsmodells gehört. Zu ihren Eingabeparametern gehören u. a. die Sicherheitsinformationen des Objekts, die Sicherheitsidentität des Threads, wie sie von ObCheckObjectAccess erfasst wurde, und die von dem Thread angeforderte Art des Zugriffs. Je nachdem, ob dem Thread der gewünschte Zugriff auf das Objekt gewährt wird oder nicht, gibt SeAccessCheck entweder true oder false zurück. Nehmen wir an, ein Thread möchte wissen, wann ein bestimmter Prozess endet (oder auf irgendeine Weise beendet wird). Dazu muss er sich ein Handle für den Zielprozess beschaffen, indem er OpenProcess aufruft und dabei zwei wichtige Argumente übergibt, nämlich die Objekte schützen Kapitel 7 717
eindeutige Prozesskennung (wobei wir davon ausgehen, dass er sie kennt oder auf irgendeine Weise erworben hat) und eine Zugriffsmaske, die die gewünschten Operationen angibt. Ein fauler Entwickler könnte nun einfach PROCESS_ALL_ACCESS als Zugriffsmaske übergeben, um alle möglichen Zugriffsrechte für den Prozess anzufordern. Dabei können die beiden folgenden Dinge passieren: QQ Wenn dem aufrufenden Thread alle Berechtigungen gewährt werden können, erhält er ein gültiges Handle zurück und kann WaitForSingleObject aufrufen, um auf die Beendigung des Prozesses zu warten. Ein anderer Thread in dem Prozess, der möglicherweise geringere Rechte hat, kann jedoch dasselbe Handle nutzen, um andere Operationen an dem Prozess vorzunehmen. Beispielsweise ist er in der Lage, den Prozess mit TerminateProcess vorzeitig zu beenden. Das ist möglich, da das Handle sämtliche Operationen an dem Prozess zulässt. QQ Wenn der Thread keine ausreichenden Rechte hat, um ihm jede mögliche Form des Zugriffs zu gewähren, schlägt der Aufruf fehl. Das Ergebnis ist ein ungültiges Handle, also keinerlei Zugriff auf den Prozess. Das hätte jedoch nicht sein müssen, da es völlig ausreichend gewesen wäre, einfach nur die Zugriffsmaske SYNCHRONIZE zu übergeben. Das hätte größere Erfolgsaussichten gehabt als die Anforderung von PROCESS_ALL_ACCESS. Daraus lässt sich die einfache Schlussfolgerung ziehen, dass ein Thread genau den gewünschten Zugriff anfordern soll – nicht mehr und nicht weniger. Der Objekt-Manager führt eine Zugriffsvalidierung auch durch, wenn ein Prozess mit einem vorhandenen Handle auf ein Objekt verweist. Solche Verweise erfolgen oft indirekt, etwa wenn ein Prozess eine Windows-API aufruft, um ein Objekt zu bearbeiten, und dabei ein Objekthandle übergibt. Beispielsweise kann ein Thread, der eine Datei öffnet, Leseberechtigungen für die Datei anfordern. Hat er gemäß seinem Sicherheitskontext und der Sicherheitseinstellungen der Datei die Berechtigung, auf diese Weise auf das Objekt zuzugreifen, erstellt der Objekt-Manager in der Handletabelle des Prozesses, zu dem der Thread gehört, ein Handle, das für die Datei steht. Zusammen mit dem Handle speichert der Objekt-Manager auch, welche Arten des Zugriffs die Threads in dem Prozess durchführen dürfen. Der Thread könnte nun später versuchen, mit der Windows-Funktion WriteFile in die Datei zu schreiben, wobei er das Dateihandle als Parameter übergibt. Der Systemdienst N­ tWriteFile, den WriteFile über Ntdll.dll aufruft, gewinnt mithilfe der (im WDK dokumentierten) Objekt-Manager-Funktion ObReferenceObjectByHandle aus diesem Handle einen Zeiger auf das Dateiobjekt. ObReferenceObjectByHandle nimmt die vom Aufrufer anforderte Art des Zugriffs als Parameter entgegen. Nachdem sie den Eintrag für das Handle in der Handletabelle des Prozesses gefunden hat, vergleicht diese Funktion den angeforderten Zugriff mit dem Zugriff, der beim Öffnen der Datei gewährt wurde. In diesem Beispiel verweigert ObReferenceObjectByHandle die Schreiboperation, da der Aufrufer beim Öffnen der Datei keinen Schreibzugriff erhalten hat. Die Windows-Sicherheitsfunktionen ermöglichen Windows-Anwendungen auch, ihre eigenen privaten Objekte zu definieren und die SRM-Dienste aufzurufen (über die weiter hinten beschriebenen AuthZ-Benutzermodus-APIs), um das Windows-Sicherheitsmodell auf diese Objekte anzuwenden. Viele Kernelmodusfunktionen, die der Objekt-Manager und andere Komponenten der Exekutive nutzen, um ihre eigenen Objekte zu schützen, werden als Windows-Benutzermodus-APIs exportiert. Das Gegenstück von SeAccessCheck im Benutzermo- 718 Kapitel 7 Sicherheit
dus ist die AuthZ-API AccessCheck. Windows-Anwendungen können daher die Flexibilität des Sicherheitsmodells nutzen und sich transparent in die Authentifizierungs- und Administrationsschnittstellen integrieren, die in Windows vorhanden sind. Im Grunde genommen ist das SRM-Sicherheitsmodell ein Algorithmus mit drei Eingaben: der Sicherheitsidentität eines Threads, der Art des Zugriffs auf ein Objekt, die der Thread anfordert, und den Sicherheitseinstellungen des Objekts. Die Ausgabe lautet entweder Ja oder Nein und gibt an, ob das Sicherheitsmodell dem Thread den gewünschten Zugriff gewährt. In den folgenden Abschnitten werden die Eingaben ausführlicher beschrieben und der Algorithmus für die Zugriffsvalidierung erklärt. Experiment: Zugriffsmasken einsehen In Process Explorer können Sie sich die Zugriffsmasken für offene Handles ansehen: 1. Starten Sie Process Explorer. 2. Öffnen Sie das Menü View und wählen Sie Lower Pane View > Handles, damit im unteren Bereich Handles angezeigt werden. 3. Rechtsklicken Sie auf die Spaltenköpfe im unteren Bereich und wählen Sie Select Col­ umns, um das folgende Dialogfeld einzublenden: 4. Aktivieren Sie die Kontrollkästchen Access Mask und Decoded Access Mask (wobei Letzteres erst ab Version 16.10 angezeigt wird) und klicken Sie auf OK. → Objekte schützen Kapitel 7 719
5. Markieren Sie in der Prozessliste Explorer.exe und schauen Sie sich die Handles im unteren Bereich an. Bei jedem Handle ist die Zugriffsmaske angegeben, die besagt, welche Art von Zugriff für dieses Handle gewährt wurde. Die rechte Spalte Decoded Access Mask zeigt für viele Arten von Objekten in Textform an, was die Bits in der Zugriffsmaske bedeuten: Wie Sie sehen, gibt es sowohl allgemeine Zugriffsrechte (wie READ_CONTROL und SYNCHRONIZE) als auch spezielle (z. B. KEY_READ und MODIFY_STATE). Bei den meisten der speziellen Zugriffsrechte werden lediglich Abkürzungen der Namen angegeben, die tatsächlich in den Windows-Headern definiert sind (wie MODIFY_STATE statt EVENT_MODIFY_STATE oder TERMINATE statt PROCESS_TERMINATE). Sicherheitskennungen Anstelle von Namen (die nicht unbedingt eindeutig sein müssen) verwendet Windows Sicherheitskennungen (Security Identifiers, SIDs) zur Bezeichnung von Entitäten, die in einem System Aktionen ausführen. Benutzer, lokale und Domänengruppen, lokale Computer, Domänen, Domänenmitglieder und Dienste – sie alle haben Sicherheitskennungen. Eine SID ist ein numerischer Wert variabler Länge, der aus der Revisionsnummer der SID-Struktur, einem 48-Bit-Wert für die Bezeichnerautorität sowie einer variablen Anzahl von 32-Bit-Werten für die Teilautorität oder den relativen Bezeichner (RID) besteht. Der Autoritätswert bezeichnet den Agent, der die SID ausgegeben hat, wobei es sich gewöhnlich um ein lokales Windows-System oder eine Domäne handelt. Teilautoritätswerte bezeichnen vertrauenswürdige Stellen relativ zur ausgebenden Autorität, und RIDs bilden einfach eine Möglichkeit, mit der Windows eindeutige SIDs auf der Grundlage einer gemeinsamen Basis-SID erstellen kann. Da SIDs lang sind und Windows darauf achtet, jede SID aus wirklich zufälligen Werten zusammenzusetzen, ist es praktisch unmöglich, dass Windows irgendwo auf der Welt zweimal dieselbe SID vergibt. 720 Kapitel 7 Sicherheit
In der Darstellung in Textform wird jeder SID das Präfix S vorangestellt, und die einzelnen Bestandteile sind durch Bindestriche voneinander abgesetzt: S-1-5-21-1463437245-1224812800-863842198-1128 Diese SID hat die Revisionsnummer 1 und den Wert 5 für die Bezeichnerautorität (was der Wert für die Windows-Sicherheitsautorität ist). Der Rest der SID besteht aus vier Teilautoritätswerten und einem RID (1128). Hierbei handelt es sich um eine Domänen-SID. SIDs für lokale Computer in dieser Domäne haben dieselbe Revisionsnummer, denselben Wert für die Bezeichnerautorität und dieselbe Anzahl von Teilautoritätswerten. Bei der Installation von Windows richtet das Setupprogramm eine Computer-SID für den Rechner ein. Für die lokalen Konten auf dem Computer weist Windows SIDs zu, die auf der SID des Rechners basieren und einen RID am Ende haben. Die RIDs für Benutzerkonten und Gruppen beginnen bei 1000 und werden für jeden neuen Benutzer und jede Gruppe um 1 erhöht. Dcpromo.exe (Domain Controller Promote), das Programm zum Erstellen einer neuen Windows Domäne verwendet die SID des Computers, der zum Domänencontroller heraufgestuft wird, als Domänen-SID. Sollte der Rechner wieder heruntergestuft werden, so wird für ihn eine neue Computer-SID erstellt. Für neue Domänenkonten gibt Windows SIDs aus, die auf der Domänen-SID beruhen und mit einem RID versehen sind (der wiederum bei 1000 beginnt und für jeden neuen Benutzer und jede neue Gruppe um 1 erhöht wird). Ein RID von 1028 bedeutet, dass es sich bei der SID um die 29. in der Domäne handelt. Für viele vordefinierte Konten und Gruppen gibt Windows SIDs aus, die aus der Computer- oder Domänen-SID und einem vordefinierten RID bestehen. Beispielsweise wird für das Administratorkonto der RID 500 verwendet und für das Gastkonto der RID 501. Für das Konto des lokalen Computeradministrators wird also der RID 500 an die Computer-SID angehängt: S-1-5-21-13124455-12541255-61235125-500 In Windows sind auch einige lokale und Domänen-SIDs für Standardgruppen vordefiniert. Beispielsweise gibt es die Jeder-SID S-1-1-0, die für »alle Konten« (außer anonymen Benutzern) steht. Die Netzwerkgruppe, die für alle über das Netzwerk an dem Computer angemeldeten Benutzer steht, hat die SID S-1-5-2. Tabelle 7–2, entnommen aus der Dokumentation des Windows SDK, zeigt die numerischen Werte und die Verwendung einiger der grundlegenden Standard-SIDs. Im Gegensatz zu den Benutzer-SIDs handelt es sich hierbei um vordefinierte Konstanten, die auf allen Windows-Systemen weltweit jeweils dieselben Werte haben. Wenn eine Datei auf dem System, auf dem sie erstellt wurde, für die Gruppe Jeder zugänglich ist, so ist sie auch für die Gruppe Jeder in jedem System und jeder Domäne zugänglich, in die die Festplatte, auf der sie sich befindet, verlegt wird. Benutzer auf diesen Systemen müssen sich jedoch erst mit einem Konto auf diesen Systemen authentifizieren, bevor sie Mitglieder der Gruppe Jeder werden. SID Name Verwendung S-1-0-0 Niemand Wird verwendet, wenn die SID unbekannt ist. S-1-1-0 Jeder Eine Gruppe, die alle Benutzer außer anonymen einschließt S-1-2-0 Lokal Benutzer, die sich über ein lokales (physisch mit dem System verbundenes) Terminal anmelden → Objekte schützen Kapitel 7 721
SID Name Verwendung S-1-3-0 Ersteller-Besitzer Eine Sicherheitskennung, die durch die Sicherheitskennung des Benutzers ersetzt wird, der ein neues Objekt erstellt hat (wird in vererbbaren Zugriffssteuerungseinträgen verwendet) S-1-3-1 Erstellergruppe Eine Sicherheitskennung, die durch die Sicherheitskennung der primären Gruppe des Benutzers ersetzt wird, der ein neues Objekt erstellt hat (wird in vererbbaren Zugriffssteu­ erungseinträgen verwendet) S-1-5-18 Lokales Systemkonto Wird von Diensten verwendet. S-1-5-19 Lokales Dienstkonto Wird von Diensten verwendet. S-1-5-20 Netzwerkdienstkonto Wird von Diensten verwendet. Tabelle 7–2 Eine Auswahl von Standard-SIDs Eine Liste vordefinierter SIDs finden Sie im Artikel 243330 der Microsoft Knowledge Base auf http://support.microsoft.com/kb/243330. Schließlich erstellt Winlogon noch eine eindeutige Anmelde-SID für jede interaktive Anmeldesitzung. Gewöhnlich werden solche SIDs in Zugriffssteuerungseinträgen verwendet, die den Zugriff für die Dauer der Anmeldesitzung eines Clients erlauben. Beispielsweise kann ein Windows-Dienst mit der Funktion LogonUser eine neue Anmeldesitzung starten. Diese Funktion gibt ein Zugriffstoken zurück, aus dem der Dienst die Anmelde-SID entnehmen kann. Anschließend kann der Dienst diese SID in einem Zugriffssteuerungseintrag verwenden (siehe den Abschnitt »Sicherheitsdeskriptoren und Zugriffssteuerung« weiter hinten in diesem Kapitel), mit dem der Anmeldesitzung des Clients der Zugriff auf die interaktive Fensterstation und den Desktop gewährt wird. Die SID für eine Anmeldesitzung hat die Form S-1-5-5-X-Y, wobei die Werte für X und Y zufällig generiert werden. Experiment: SIDs mit PsGetSid und Process Explorer einsehen Mit dem Sysinternals-Tool PsGetSid können Sie sich die SID aller Konten ansehen, die Sie verwenden. Die Optionen von PsGetSid machen es möglich, die Namen von Computerund Benutzerkonten in die zugehörigen SIDs zu übersetzen und umgekehrt. Wenn Sie das Programm PsGetSid ohne Optionen ausführen, gibt es die SID des lokalen Computers aus. Da das Administratorkonto stets den RID 500 hat, können Sie ermitteln, welchen Namen dieses Konto hat (wenn es aus Sicherheitsgründen umbenannt wurde), indem Sie einfach die Computer-SID mit angehängtem -500 als Befehlszeilenargument an PsGetSid übergeben. Um die SID eines Domänenkontos abzurufen, geben Sie den Benutzernamen mit vorangestelltem Domänennamen ein: c:\>psgetsid redmond\johndoe → 722 Kapitel 7 Sicherheit
Die SID einer Domäne erhalten Sie, indem Sie ihren Namen als Argument von PsGetSid angeben: c:\>psgetsid redmond Aus dem RID Ihres eigenen Kontos können Sie zumindest ablesen, wie viele Sicherheitskonten in Ihrer Domäne oder auf Ihrem lokalen Computer erstellt wurden ( je nachdem, ob Sie ein Domänen- oder ein lokales Computerkonto verwenden), indem Sie 999 vom RIDWert subtrahieren. Um zu ermitteln, welchem Konto ein bestimmter RID zugewiesen wurde, übergeben Sie eine SID mit diesem RID an PsGetSid. Wenn PsGetSid meldet, dass der SID kein Konto zugeordnet werden konnte, obwohl der RID kleiner ist als der für Ihr Konto, dann bedeutet das, dass das Konto mit diesem RID gelöscht wurde. Um beispielsweise den Namen des Kontos herauszufinden, dem der 28. RID zugewiesen wurde, übergeben Sie die Domänen-SID mit angehängtem -1027 an PsGetSid: c:\>psgetsid S-1-5-21-1787744166-3910675280-2727264193-1027 Account for S-1-5-21-1787744166-3910675280-2727264193-1027: User: redmond\johndoe Informationen über Konto- und Gruppen-SIDs können Sie auch auf der Registerkarte Security von Process Explorer einsehen. Dort ist zu sehen, wer der Besitzer des untersuchten Prozesses ist und zu welchen Gruppen dieses Konto gehört. Doppelklicken Sie dazu einfach auf einen beliebigen Prozess (z. B. Explorer.exe) und rufen Sie dann die Registerkarte Security auf. Dadurch erhalten Sie eine Anzeige wie die folgende: → Objekte schützen Kapitel 7 723
Im Feld User wird der Anzeigename des Kontos angegeben, dem der Prozess gehört. Das Feld SID zeigt den SID-Wert, und die Liste Groups führt alle Gruppen auf, zu denen das Konto gehört. (Gruppen werden weiter hinten in diesem Kapitel beschrieben.) Integritätsebenen Wie bereits erwähnt, können Integritätsebenen den besitzerverwalteten Zugriff überschreiben, um zwischen einem Prozess und den Objekten zu unterscheiden, die als derselbe Benutzer ausgeführt werden und ihm gehören. Dadurch ist es möglich, Code und Daten innerhalb eines Benutzerkontos zu separieren. Mit dem MIC-Mechanismus (Mandatory Integrity Control), der einen Aufrufer mit einer Integritätsebene verknüpft, kann der SRM ausführlichere Informationen über die Natur dieses Aufrufers gewinnen. Der Mechanismus bietet auch Informationen über die erforderlichen Vertrauensstufen für den Zugriff auf das Objekt, indem er eine Integritätsebene dafür definiert. Die Integritätsebene eines Tokens lässt sich mit der API GetTokenInformation und dem Auflistungswert TokenIntegrityLevel abrufen. Bezeichnet werden die Integritätsebenen anhand einer SID. Sie können zwar beliebige Werte annehmen, aber zur Trennung von Rechteebenen verwendet das System die sechs in Tabelle 7–3 aufgeführten Hauptebenen. SID Name (Ebene) Verwendung S-1-16-0x0 Nicht vertrauenswürdig (0) Wird von Prozessen verwendet, die von der Gruppe Anonym gestartet werden. Diese Ebene blockiert die meisten Schreibzugriffe. S-1-16-0x1000 Niedrig (1) Wird von Anwendungscontainerprozessen (UWP) und Internet Explorer im geschützten Modus verwendet. Diese Ebene blockiert den Schreibzugriff auf die meisten Objekte auf dem System (darunter Dateien und Registrierungsschlüssel). S-1-16-0x2000 Mittel (2) Wird von normalen Anwendungen verwendet, die bei aktiver Benutzerkontensteuerung gestartet wurden. S-1-16-0x3000 Hoch (3) Wird von administrativen Anwendungen ver­ wendet, die bei aktiver Benutzerkontensteuerung mit erhöhten Rechten gestartet wurden, sowie von normalen Anwendungen, wenn die Benutzer­ kontensteuerung ausgeschaltet und der Benutzer ein Administrator ist. S-1-16-0x4000 System (4) Wird von Diensten und anderen Prozessen auf Systemebene verwendet (wie Wininit, Winlogon, Smss usw.). S-1-16-0x5000 Geschützt (5) Wird zurzeit standardmäßig nicht verwendet und kann nur von Aufrufern im Kernelmodus festgelegt werden. Tabelle 7–3 SIDs für Integritätsebenen 724 Kapitel 7 Sicherheit
Scheinbar gibt es noch eine weitere Integritätsebene, die AppContainer genannt und von UWP-Anwendungen verwendet wird. Tatsächlich ist sie jedoch identisch mit der Ebene Niedrig. UWP-Prozesstokens verfügen über ein Attribut, um anzugeben, dass sie in einem Anwendungscontainer laufen (siehe den entsprechenden Abschnitt weiter hinten). Diese Information kann mit der API GetTokenInformation und dem Auflistungswert TokenIsAppContainer abgerufen werden. Experiment: Die Integritätsebenen von Prozessen einsehen In Process Explorer können Sie sich die Integritätsebenen von Prozessen auf Ihrem System ansehen, wie das folgende Beispiel zeigt: 1. Starten Sie Microsoft Edge und Calc.exe (Windows 10). 2. Öffnen Sie eine Eingabeaufforderung mit erhöhten Rechten. 3. Öffnen Sie den Editor auf normale Weise (also ohne erhöhte Rechte). 4. Starten Sie Process Explorer mit erhöhten Rechten, rechtsklicken Sie auf einen beliebigen Spaltenkopf in der Prozessliste und wählen Sie Select Columns. 5. Rufen Sie die Registerkarte Process Image auf und aktivieren Sie das Kontrollkästchen Integrity Level. Das Dialogfeld sollte jetzt wie folgt aussehen: → Objekte schützen Kapitel 7 725
6. Process Explorer zeigt jetzt die Integritätsebenen der Prozesse auf Ihrem System an. Der Editor (Notepad) sollte auf mittlerer Ebene ausgeführt werden (Medium), Edge (MicrosoftEdge.exe) auf der Ebene AppContainer und die Eingabeaufforderung mit erhöhten Rechten auf High. Dienste und Systemprozesse laufen auf einer noch höheren Integritätsebene, nämlich System. Jeder Prozess ist einer Integritätsebene zugewiesen. Sie wird in seinem Token vermerkt und nach den folgenden Regeln vererbt: QQ Ein Prozess erbt normalerweise die Integritätsebene seines Elternprozesses (was beispielsweise bedeutet, dass eine Eingabeaufforderung mit erhöhten Rechten weitere Prozesse mit erhöhten Rechten hervorruft). QQ Wenn das Dateiobjekt für das ausführbare Abbild, zu dem der Kindprozess gehört, über eine eigene Integritätsebene verfügt, und der Elternprozess die mittlere oder hohe Integritätsebene hat, dann erbt der Kindprozess die niedrigere der beiden Ebenen. QQ Ein Elternprozess kann Kindprozesse erstellen, deren Integritätsebene ausdrücklich niedriger ist als seine eigene. Dazu dupliziert er sein Zugriffstoken mit DuplicateTokenEx, ändert die Integritätsebene im neuen Token mit SetTokenInformation auf den gewünschten Wert und ruft dann CreateProcessAsUser mit dem neuen Token auf. In Tabelle 7–3 haben Sie die Integritätsebenen für Prozesse gesehen, aber was ist mit Objekten? Auch in den Sicherheitsdeskriptoren von Objekten ist eine Integritätsebene gespeichert, und zwar in einer Struktur, die als verbindliche Beschriftung bezeichnet wird. 726 Kapitel 7 Sicherheit
Um die Migration von früheren Windows-Versionen zu ermöglichen (deren Registrierungsschlüssel und Dateien keine Informationen über Integritätsebenen enthalten) und um Anwendungsentwicklern die Arbeit zu erleichtern, verfügen alle Objekte über eine implizite Integritätsebene, sodass es nicht erforderlich ist, eine Ebene manuell anzugeben. Diese implizite Ebene ist die mittlere, was bedeutet, dass die erforderliche Richtlinie (mit der wir uns in Kürze genauer beschäftigen werden) für das Objekt an Tokens ausgeführt wird, die auf einer niedrigeren als der mittleren Integritätsebene auf das Objekt zugreifen. Wenn ein Prozess ein Objekt erstellt, ohne eine Integritätsebene anzugeben, prüft das System die Integritätsebene in dem Token. Bei Tokens mit mittlerer oder hoher Integritätsebene bleibt die implizite mittlere Integritätsebene des Objekts erhalten. Enthält ein Token aber eine niedrigere Integritätsebene, so wird das Objekt mit einer ausdrücklichen Integritätsebene gleich derjenigen aus dem Token erstellt. Objekte, die von Prozessen mit der hohen oder der Systemintegritätsebene erstellt werden, haben selbst die mittlere Integritätsebene, damit Benutzer die Benutzerkontensteuerung aus- und einschalten können. Würden Objekte stets die Integritätsebene ihres Erstellers erben, können Anwendungen eines Administrators fehlschlagen, der die Benutzerkontensteuerung ausschaltet und anschließend wieder einschaltet, da der Administrator bei der Ausführung auf der hohen Integritätsebene nicht in der Lage wäre, Registrierungseinstellungen oder Dateien zu ändern. Objekte können auch über eine ausdrückliche Integritätsebene verfügen, die vom System oder vom Ersteller festgelegt wird. Wenn beispielsweise der Kernel Prozesse, Threads, Tokens und Jobs erstellt, gibt er ihnen eine solche ausdrückliche Integritätsebene. Dadurch soll verhindert werden, dass ein Prozess desselben Benutzers, der auf einer niedrigeren Integritätsebene ausgeführt wird, auf diese Objekte zugreift und ihren Inhalt oder ihr Verhalten ändert (z. B. durch DLL-Injektion oder Codeänderung). Neben einer Integritätsebene haben Objekte auch eine erforderliche Richtlinie. Sie legt den tatsächlichen Grad an Schutz fest, der aufgrund der Überprüfung der Integritätsebene angewendet wird. Wie Tabelle 7–4 zeigt, sind dabei drei Arten von Schutz möglich. Die Integritätsebene und die erforderliche Richtlinie werden zusammen im selben Zugriffssteuerungseintrag gespeichert. Richtlinie Standardmäßige Verwendung Beschreibung No-Write-Up Implizit für alle Objekte Schränkt den Schreibzugriff auf das Objekt für Prozesse mit einer niedrigeren Integritätsebene ein. No-Read-Up Nur für Prozessobjekte Schränkt den Lesezugriff auf das Objekt für Prozesse mit einer niedrigeren Integritätsebene ein. Die Anwendung auf Prozessobjekte schützt vor Informationslecks, da externe Prozesse daran gehindert werden, im Adressraum zu lesen. No-Execute-Up Nur für Binärdateien, die COM-Klassen implementieren Schränkt den Ausführungszugriff auf das Objekt für Prozesse mit einer niedrigeren Integritätsebene ein. Die Anwendung auf COM-Klassen schränkt die Startaktivierungsberechtigungen für diese Klassen ein. Tabelle 7–4 Erforderliche Richtlinien von Objekten Objekte schützen Kapitel 7 727
Experiment: Die Integritätsebenen von Objekten einsehen Mit dem Sysinternals-Tools AccessChk können Sie sich die Integritätsebenen von Objekten auf Ihrem System wie Dateien, Prozessen und Registrierungsschlüsseln ansehen. Das folgende Experiment zeigt dabei, welchen Zweck das Verzeichnis LocalLow in Windows hat: 1. Wechseln Sie an einer Eingabeaufforderung zu C:\Users\<Benutzername>, wobei Sie für <Benutzername> Ihren Benutzernamen einsetzen. 2. Versuchen Sie, AccessChk wie folgt für den Ordner AppData auszuführen: C:\Users\UserName> accesschk –v appdata 3. Beachten Sie den Unterschied zwischen den Unterordnern Local und LocalLow in der Ausgabe: C:\Users\UserName\AppData\Local Medium Mandatory Level (Default) [No-Write-Up] [...] C:\Users\UserName\AppData\LocalLow Low Mandatory Level [No-Write-Up] [...] C:\Users\UserName\AppData\Roaming Medium Mandatory Level (Default) [No-Write-Up] [...] 4. Wie Sie sehen, hat das Verzeichnis LocalLow die Integritätsebene Low, während die Verzeichnisse Local und Roaming über die Integritätsebene Medium (default) verfügen. Die Angabe default bedeutet, dass das System die implizite Integritätsebene verwendet. 5. Führen Sie AccessChk erneut für den Ordner AppData aus, geben Sie diesmal aber den Schalter -e an, damit nur die ausdrücklichen Integritätsebenen angezeigt werden. Jetzt sehen Sie nur Angaben zu LocalLow. Mit den Schaltern -o (Objekt), -k (Registrierungsschlüssel) und -p (Prozess) können Sie sich andere Objekte als Dateien und Verzeichnisse anzeigen lassen. Tokens Der SRM verwendet ein als Token (oder Zugriffstoken) bezeichnetes Objekt, um den Sicherheitskontext eines Prozesses oder Threads zu identifizieren. Ein solcher Sicherheitskontext besteht aus den Informationen zur Beschreibung des Kontos, der Gruppen und der Rechte, die mit dem Prozess bzw. Thread verknüpft sind. Darüber hinaus enthalten Tokens auch Informationen wie die Sitzungs-ID, die Integritätsebene und den Virtualisierungsstatus der Benutzerkontensteuerung. (Sowohl Rechte als auch den Virtualisierungsmechanismus der Benutzerkontensteuerung werden wir weiter hinten in diesem Kapitel noch ausführlicher beschreiben.) 728 Kapitel 7 Sicherheit
Wenn sich ein Benutzer anmeldet, erstellt Lsass ein erstes Token für ihn und ermittelt dann, ob er ein Mitglied einer privilegierten Gruppe ist oder über ein hohes Recht verfügt. In diesem Schritt wird nach Mitgliedschaft in einer der folgenden Gruppen gesucht: QQ Integrierte Administratorengruppe QQ Zertifikatadministratoren QQ Domänenadministratoren QQ Unternehmensadministratoren QQ Richtlinienadministratoren QQ Schemaadministratoren QQ Domänencontroller QQ Schreibgeschützte Domänencontroller der Organisation QQ Schreibgeschützte Domänencontroller QQ Konten-Operatoren QQ Sicherungsoperatoren QQ Kryptografie-Operatoren QQ Netzwerkkonfigurations-Operatoren QQ Druck-Operatoren QQ System-Operatoren QQ RAS-Server QQ Hauptbenutzer QQ Prä-Windows 2000-kompatibler Zugriff Viele der hier genannten Gruppen werden ausschließlich auf Systemen verwendet, die an eine Domäne angeschlossen sind, und räumen ihren Mitgliedern keine lokalen Administratorrechte ein. Stattdessen ist es den Mitgliedern gestattet, domänenweite Einstellungen zu ändern. Bei den hohen Rechten, nach denen gesucht wird, handelt es sich um folgende: QQ SeBackupPrivilege QQ SeCreateTokenPrivilege QQ SeDebugPrivilege QQ SeImpersonatePrivilege QQ SeLabelPrivilege QQ SeLoadDriverPrivilege QQ SeRestorePrivilege QQ SeTakeOwnershipPrivilege QQ SeTcbPrivilege Eine ausführliche Beschreibung erhalten Sie im Abschnitt »Rechte« weiter hinten in diesem Kapitel. Objekte schützen Kapitel 7 729
Sind eine oder mehrere dieser Gruppen oder Rechte vorhanden, erstellt Lsass ein eingeschränktes Token für den Benutzer (ein gefiltertes Administratortoken) und eine Anmeldesitzung für beide. Das Standardbenutzertoken wird an den ersten Prozess oder die ersten Prozesse angehängt, die Winlogon starten (standardmäßig ist das Userinit.exe). Wurde die Benutzerkontensteuerung ausgeschaltet, so haben Administratoren ein Token, das die Mitgliedschaft in ihren Administratorgruppen und ihre Rechte einschließt. Da Kindprozesse standardmäßig eine Kopie des Tokens ihres Erstellers erben, laufen alle Prozesse in der Sitzung eines Benutzers unter demselben Token. Um ein Token zu generieren, können Sie auch die Windows-Funktion LogonUser verwenden. Anschließend können Sie mit diesem Token einen Prozess anlegen, der im Sicherheitskontext des mit LogonUser angemeldeten Benutzers läuft, indem Sie der Windows-Funktion CreateProcessAsUser dieses Token übergeben. Die Funktion CreateProcessWithLogonW kombiniert diese beiden Vorgänge in einem einzigen Aufruf. Auf diese Weise startet der Befehl Runas Prozesse unter alternativen Tokens. Da Benutzerkonten über verschiedene Mengen von Rechten und zugehörigen Gruppenkonten verfügen, weisen ihre Tokens unterschiedliche Größen auf. Allerdings enthalten alle Tokens die gleichen Arten von Informationen. Die wichtigsten Inhalte sehen Sie in Abb. 7–7. Tokenquelle Token-ID Authentifizierungs-ID Elterntoken-ID Ablaufzeit Tokensperre Modifizierungs-ID Rechte Überwachungsrichtlinie Sitzungs-ID SID der primären Gruppe Gruppen SID Gruppe 1 SID Gruppe 2 Standard-DACLs Eingeschränkte SIDs Tokentyp Eingeschränkte SID 1 Eingeschränkte SID 2 SID Fähigkeit 1 SID Fähigkeit 2 Ebene des Identitätswechsels Flags Anmeldesitzung Paket-SID Fähigkeiten LowBox-Informationen Abbildung 7–7 Zugriffstokens 730 Kapitel 7 Sicherheit
Die Sicherheitsmechanismen in Windows bestimmen mithilfe von zwei Komponenten, welche Objekte zugänglich sind und welche Operationen durchgeführt werden können. Die eine dieser Komponente besteht aus den Tokenfeldern für die Benutzerkonto-SID und die Gruppen-SIDs. Der SRM ermittelt anhand der SIDs, ob ein Prozess oder Thread den angeforderten Zugriff auf ein sicherungsfähiges Objekt (z. B. eine NTFS-Datei) erhalten kann. Die Gruppen-SIDs in einem Token geben an, in welchen Gruppen das Benutzerkonto Mitglied ist. Dabei kann beispielsweise eine Serveranwendung bestimmte Gruppen deaktivieren, um die Anmeldeinformation eines Tokens einzuschränken, wenn die Anwendung von einem Client angeforderte Aktionen durchführt. Die Deaktivierung einer Gruppe wirkt sich fast genauso aus, als wäre die Gruppe in dem Token gar nicht vorhanden. (Dies führt zu einer reinen Verweigerungsgruppe, was im Abschnitt »Eingeschränkte Tokens« beschrieben wird. Deaktivierte SIDs werden im Rahmen von Zugriffsprüfungen verwendet, die im Abschnitt »Den Zugriff bestimmen« weiter hinten erläutert werden.) Zu den Gruppen-SIDs kann auch eine besondere SID mit der Integritätsebene des Prozesses oder Threads gehören. Der SRM verwendet noch ein weiteres Feld in dem Token, das die erforderliche Integritätsrichtlinie beschreibt, um die weiter hinten in diesem Kapitel beschriebenen verbindlichen Integritätsprüfungen durchzuführen. Die zweite Komponente, anhand der bestimmt wird, was der Thread oder Prozess tun kann, ist das Array der Rechte, die mit dem Token verknüpft sind, beispielsweise das Recht, den Computer herunterzufahren. Weiter hinten in diesem Kapitel werden Rechte noch ausführlicher behandelt. Die Standardfelder eines Tokens für die primäre Gruppe und für die besitzerverwaltete Zugriffssteuerungsliste (DACL) sind Sicherheitsattribute, die Windows auf die unter Verwendung des Tokens von einem Prozess oder Thread erstellten Objekte anwendet. Durch die Aufnahme der Sicherheitsinformationen in die Tokens erleichtert Windows es Prozessen und Threads, Objekte mit Standardsicherheitsattributen zu erstellen, da die Prozesse und Threads dadurch nicht mehr gezwungen sind, diskrete Sicherheitsinformationen für jedes Objekt anzufordern, das sie erstellen. Beim Typ eines Tokens wird zwischen einem Primärtoken (das den Sicherheitskontext eines Prozesses angibt) und einem Identitätswechseltoken unterschieden (das von Threads verwendet wird, um vorübergehend einen anderen Sicherheitskontext anzunehmen, gewöhnlich den eines anderen Benutzers). Identitätswechseltokens können eine Identitätswechselebene enthalten, die angibt, welche Art von Identitätswechsel in dem Token aktiv ist. (Identitätswechsel werden weiter hinten in diesem Kapitel beschrieben.) Ein Token gibt auch die erforderliche Richtlinie für den Prozess oder Thread an. Sie legt fest, wie MIC bei der Verarbeitung des Tokens vorgeht. Dabei gibt es die beiden folgenden Richtlinien: QQ TOKEN_MANDATORY_NO_WRITE_UP Diese Richtlinie ist standardmäßig aktiviert und legt die Richtlinie No-Write-Up für das Token fest. Das bedeutet, dass der Prozess oder Thread keinen Schreibzugriff auf Objekte mit einer höheren Integritätsebene hat. QQ TOKEN_MANDATORY_NEW_PROCESS_MIN Diese Richtlinie ist ebenfalls standardmäßig aktiviert und legt fest, dass sich der SRM beim Starten eines Kindprozesses die Integritätsebene des ausführbaren Abbilds ansehen und für den Kindprozess die kleinere der beiden Integritätsebenen des Elternprozesses und des Dateiobjekts festlegen soll. Objekte schützen Kapitel 7 731
Die Tokenflags schließen Parameter ein, die das Verhalten einzelner Mechanismen der Benutzerkontensteuerung und von UIPI bestimmen, z. B. Virtualisierung und Zugriff auf die Benutzerschnittstelle. Diese Mechanismen werden weiter hinten in diesem Kapitel beschrieben. Wenn AppLocker-Regeln definiert wurden, kann jedes Token auch Attribute enthalten, die vom Anwendungsidentifizierungsdienst (einem Teil von AppLocker) zugewiesen wurden. AppLocker und die Verwendung von AppLocker-Attributen in Zugriffstokens werden weiter hinten in diesem Kapitel beschrieben. Ein Token für einen UWP-Prozess enthält auch Informationen über den Anwendungscontainer, in dem der Prozess untergebracht ist. An erster Stelle steht dabei die Paket-SID für das UWP-Paket, aus dem der Prozess stammt. Was es mit dieser SID auf sich hat, erfahren Sie im Abschnitt »Anwendungscontainer« weiter hinten in diesem Kapitel. Zweitens müssen UWP-Prozesse Fähigkeiten für Operationen anfordern, die einer Zustimmung des Benutzers bedürfen. Zu diesen Fähigkeiten gehören der Netzwerkzugriff, die Verwendung der Telefonfähigkeiten des Geräts (falls vorhanden), der Zugriff auf die Kamera des Geräts usw. Jede dieser Fähigkeiten wird durch eine SID dargestellt, die im Token gespeichert ist. (Fähigkeiten werden ebenfalls im Abschnitt »Anwendungscontainer« beschrieben.) Die restlichen Felder im Token sind rein informativ. Das Feld für die Tokenquelle gibt in Textform an, welche Entität das Token erstellt hat. Programme, die den Ursprung eines Tokens in Erfahrung bringen wollen, können anhand dieses Feldes zwischen Quellen wie dem Windows-Sitzungs-Manager, einem Netzwerkdateiserver und einem RPC-Server unterscheiden. Die Token-ID ist ein lokal eindeutiger Bezeichner (LUID), den der SRM zuweist, wenn er das Token erstellt. Die Windows-Exekutive führt einen monoton steigenden LUID-Zähler, um jedem Token einen eindeutigen numerischen Wert zuzuweisen. LUIDs sind nur bis zum Herunterfahren des Systems garantiert eindeutig. Die Authentifizierungs-ID ist ein weiterer LUID. Er wird vom Ersteller des Tokens zugewiesen, wenn er die Funktion LsaLogonUser aufruft. Gibt der Ersteller keinen LUID an, bezieht Lsass den LUID vom LUID-Zähler der Exekutive. Lsass verwendet für alle Tokens, die von einem ursprünglichen Anmeldetoken abstammen, dieselbe Authentifizierungs-ID. Ein Programm kann die Authentifizierungs-ID eines Tokens abrufen, um zu sehen, ob das Token zur selben Anmeldesitzung gehört wie andere Tokens, die das Programm untersucht hat. Mithilfe des LUID-Zählers der Exekutive wird die Modifizierungs-ID jedes Mal aktualisiert, wenn die Eigenschaften eines Tokens geändert wurden. Anwendungen können die Modifizierungs-ID überprüfen, um zu erkennen, ob der Sicherheitskontext seit seiner letzten Verwendung verändert wurde. Tokens enthalten auch ein Feld für eine Ablaufzeit. Damit können Anwendungen eigene Sicherheitsmaßnahmen ausführen, bei denen Tokens nach einem bestimmten Zeitraum nicht mehr akzeptiert werden. Windows selbst erzwingt jedoch keinen Ablauf von Tokens. Um die Sicherheit des Systems zu garantieren, sind die Felder in einem Token nicht veränderbar (da sie sich im Kernelspeicher befinden). Zwar können einige Felder durch besondere Systemaufrufe bearbeitet werden (sofern der Aufrufer die entsprechenden Zugriffsrechte für das Tokenobjekt hat), doch Daten wie Rechte und SIDs in einem Token lassen sich vom Benutzermodus aus niemals verändern. 732 Kapitel 7 Sicherheit
Experiment: Zugriffstoken einsehen Der Kerneldebuggerbefehl dt _TOKEN zeigt das Format eines internen Tokenobjekts an. Diese Struktur unterscheidet sich zwar von der Benutzermodus-Tokenstruktur, die von Sicherheitsfunktionen der Windows-API zurückgegeben wird, doch die Felder sind ähnlich. Weitere Informationen über Tokens finden Sie in der Dokumentation zum Windows SDK. In Windows 10 sieht die Tokenstruktur wie folgt aus: lkd> dt nt!_token +0x000 TokenSource : _TOKEN_SOURCE +0x010 TokenId : _LUID +0x018 AuthenticationId : _LUID +0x020 ParentTokenId : _LUID +0x028 ExpirationTime : _LARGE_INTEGER +0x030 TokenLock : Ptr64 _ERESOURCE +0x038 ModifiedId : _LUID +0x040 Privileges : _SEP_TOKEN_PRIVILEGES +0x058 AuditPolicy : _SEP_AUDIT_POLICY +0x078 SessionId : Uint4B +0x07c UserAndGroupCount : Uint4B +0x080 RestrictedSidCount : Uint4B +0x084 VariableLength : Uint4B +0x088 DynamicCharged : Uint4B +0x08c DynamicAvailable : Uint4B +0x090 DefaultOwnerIndex : Uint4B +0x098 UserAndGroups : Ptr64 _SID_AND_ATTRIBUTES +0x0a0 RestrictedSids : Ptr64 _SID_AND_ATTRIBUTES +0x0a8 PrimaryGroup : Ptr64 Void +0x0b0 DynamicPart : Ptr64 Uint4B +0x0b8 DefaultDacl : Ptr64 _ACL +0x0c0 TokenType : _TOKEN_TYPE +0x0c4 ImpersonationLevel : _SECURITY_IMPERSONATION_LEVEL +0x0c8 TokenFlags : Uint4B +0x0cc TokenInUse : UChar +0x0d0 IntegrityLevelIndex : Uint4B +0x0d4 MandatoryPolicy : Uint4B +0x0d8 LogonSession : Ptr64 _SEP_LOGON_SESSION_REFERENCES +0x0e0 OriginatingLogonSession : _LUID +0x0e8 SidHash : _SID_AND_ATTRIBUTES_HASH +0x1f8 RestrictedSidHash : _SID_AND_ATTRIBUTES_HASH +0x308 pSecurityAttributes : Ptr64 _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION → Objekte schützen Kapitel 7 733
+0x310 Package : Ptr64 Void +0x318 Capabilities : Ptr64 _SID_AND_ATTRIBUTES +0x320 CapabilityCount : Uint4B +0x328 CapabilitiesHash : _SID_AND_ATTRIBUTES_HASH +0x438 LowboxNumberEntry : Ptr64 _SEP_LOWBOX_NUMBER_ENTRY +0x440 LowboxHandlesEntry : Ptr64 _SEP_LOWBOX_HANDLES_ENTRY +0x448 pClaimAttributes : Ptr64 _AUTHZBASEP_CLAIM_ATTRIBUTES_COLLECTION +0x450 TrustLevelSid : Ptr64 Void +0x458 TrustLinkedToken : Ptr64 _TOKEN +0x460 IntegrityLevelSidValue : Ptr64 Void +0x468 TokenSidValues : Ptr64 _SEP_SID_VALUES_BLOCK +0x470 IndexEntry : Ptr64 _SEP_LUID_TO_INDEX_MAP_ENTRY +0x478 DiagnosticInfo : Ptr64 _SEP_TOKEN_DIAG_TRACK_ENTRY +0x480 SessionObject : Ptr64 Void +0x488 VariablePart : Uint8B Das Token eines Prozesses können Sie mit dem Befehl !token untersuchen. Die Adresse des Tokens finden Sie in der Ausgabe des Befehls !process. Das folgende Beispiel zeigt den Vorgang für den Prozess Explorer.exe: lkd> !process 0 1 explorer.exe PROCESS ffffe18304dfd780 SessionId: 1 Cid: 23e4 Peb: 00c2a000 ParentCid: 2264 DirBase: 2aa0f6000 ObjectTable: ffffcd82c72fcd80 HandleCount: <Data Not Accessible> Image: explorer.exe VadRoot ffffe18303655840 Vads 705 Clone 0 Private 12264. Modified 376410. Locked 18. DeviceMap ffffcd82c39bc0d0 Token ffffcd82c72fc060 ... PROCESS ffffe1830670a080 SessionId: 1 Cid: 27b8 Peb: 00950000 ParentCid: 035c DirBase: 2cba97000 ObjectTable: ffffcd82c7ccc500 HandleCount: <Data Not Accessible> Image: explorer.exe VadRoot ffffe183064e9f60 Vads 1991 Clone 0 Private 19576. Modified 87095. Locked 0. DeviceMap ffffcd82c39bc0d0 Token ffffcd82c7cd9060 ... → 734 Kapitel 7 Sicherheit
lkd> !token ffffcd82c72fc060 _TOKEN 0xffffcd82c72fc060 TS Session ID: 0x1 User: S-1-5-21-3537846094-3055369412-2967912182-1001 User Groups: 00 S-1-16-8192 Attributes - GroupIntegrity GroupIntegrityEnabled 01 S-1-1-0 Attributes - Mandatory Default Enabled 02 S-1-5-114 Attributes - DenyOnly 03 S-1-5-21-3537846094-3055369412-2967912182-1004 Attributes - Mandatory Default Enabled 04 S-1-5-32-544 Attributes - DenyOnly 05 S-1-5-32-578 Attributes - Mandatory Default Enabled 06 S-1-5-32-559 Attributes - Mandatory Default Enabled 07 S-1-5-32-545 Attributes - Mandatory Default Enabled 08 S-1-5-4 Attributes - Mandatory Default Enabled 09 S-1-2-1 Attributes - Mandatory Default Enabled 10 S-1-5-11 Attributes - Mandatory Default Enabled 11 S-1-5-15 Attributes - Mandatory Default Enabled 12 S-1-11-96-3623454863-58364-18864-2661722203-1597581903-12253128352511459453-1556397606-2735945305-1404291241 Attributes - Mandatory Default Enabled 13 S-1-5-113 Attributes - Mandatory Default Enabled 14 S-1-5-5-0-1745560 Attributes - Mandatory Default Enabled LogonId 15 S-1-2-0 Attributes - Mandatory Default Enabled 16 S-1-5-64-36 Attributes - Mandatory Default Enabled Primary Group: S-1-5-21-3537846094-3055369412-2967912182-1001 → Objekte schützen Kapitel 7 735
Privs: 19 0x000000013 SeShutdownPrivilege Attributes - 23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default 25 0x000000019 SeUndockPrivilege Attributes - 33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes - 34 0x000000022 SeTimeZonePrivilege Attributes - Authentication ID: (0,1aa448) Impersonation Level: Anonymous TokenType: Primary Source: User32 TokenFlags: 0x2a00 ( Token in use ) Token ID: 1be803 ParentToken ID: 1aa44b Modified ID: (0, 43d9289) RestrictedSidCount: 0 RestrictedSids: 0x0000000000000000 OriginatingLogonSession: 3e7 PackageSid: (null) CapabilityCount: 0 Capabilities: 0x0000000000000000 LowboxNumberEntry: 0x0000000000000000 Security Attributes: Unable to get the offset of nt!_AUTHZBASEP_SECURITY_ATTRIBUTE.ListLink Process Token TrustLevelSid: (null) Da der Explorer nicht in einem Anwendungscontainer läuft, gibt es für ihn keine Paket-SID. Führen Sie Calc.exe in Windows 10 aus. Dadurch wird Calculator.exe erzeugt (was jetzt eine UWP-Anwendung ist). Untersuchen Sie das Token: lkd> !process 0 1 calculator.exe PROCESS ffffe18309e874c0 SessionId: 1 Cid: 3c18 Peb: cd0182c000 ParentCid: 035c DirBase: 7a15e4000 ObjectTable: ffffcd82ec9a37c0 HandleCount: <Data Not Accessible> Image: Calculator.exe VadRoot ffffe1831cf197c0 Vads 181 Clone 0 Private 3800. Modified 3746. Locked 503. DeviceMap ffffcd82c39bc0d0 Token ffffcd82e26168f0 ... lkd> !token ffffcd82e26168f0 _TOKEN 0xffffcd82e26168f0 TS Session ID: 0x1 User: S-1-5-21-3537846094-3055369412-2967912182-1001 User Groups: 00 S-1-16-4096 → 736 Kapitel 7 Sicherheit
Attributes - GroupIntegrity GroupIntegrityEnabled 01 S-1-1-0 Attributes - Mandatory Default Enabled 02 S-1-5-114 Attributes - DenyOnly 03 S-1-5-21-3537846094-3055369412-2967912182-1004 Attributes - Mandatory Default Enabled 04 S-1-5-32-544 Attributes - DenyOnly 05 S-1-5-32-578 Attributes - Mandatory Default Enabled 06 S-1-5-32-559 Attributes - Mandatory Default Enabled 07 S-1-5-32-545 Attributes - Mandatory Default Enabled 08 S-1-5-4 Attributes - Mandatory Default Enabled 09 S-1-2-1 Attributes - Mandatory Default Enabled 10 S-1-5-11 Attributes - Mandatory Default Enabled 11 S-1-5-15 Attributes - Mandatory Default Enabled 12 S-1-11-96-3623454863-58364-18864-2661722203-1597581903-12253128352511459453-1556397606-2735945305-1404291241 Attributes - Mandatory Default Enabled 13 S-1-5-113 Attributes - Mandatory Default Enabled 14 S-1-5-5-0-1745560 Attributes - Mandatory Default Enabled LogonId 15 S-1-2-0 Attributes - Mandatory Default Enabled 16 S-1-5-64-36 Attributes - Mandatory Default Enabled Primary Group: S-1-5-21-3537846094-3055369412-2967912182-1001 Privs: 19 0x000000013 SeShutdownPrivilege Attributes - 23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default 25 0x000000019 SeUndockPrivilege Attributes - 33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes - 34 0x000000022 SeTimeZonePrivilege Attributes - Authentication ID: (0,1aa448) → Objekte schützen Kapitel 7 737
Impersonation Level: Anonymous TokenType: Primary Source: User32 TokenFlags: 0x4a00 ( Token in use ) Token ID: 4ddb8c0 ParentToken ID: 1aa44b Modified ID: (0, 4ddb8b2) RestrictedSidCount: 0 RestrictedSids: 0x0000000000000000 OriginatingLogonSession: 3e7 PackageSid: S-1-15-2-466767348-3739614953-2700836392-1801644223-42277506571087833535-2488631167 CapabilityCount: 1 Capabilities: 0xffffcd82e1bfccd0 Capabilities: 00 S-1-15-3-466767348-3739614953-2700836392-1801644223-4227750657-10878335352488631167 Attributes - Enabled LowboxNumberEntry: 0xffffcd82fa2c1670 LowboxNumber: 5 Security Attributes: Unable to get the offset of nt!_AUTHZBASEP_SECURITY_ATTRIBUTE.ListLink Process Token TrustLevelSid: (null) Wie Sie sehen, ist für den Taschenrechner eine Fähigkeit erforderlich (die identisch mit der RID seines Anwendungscontainers ist, wie im Abschnitt »Anwendungscontainer« weiter hinten in diesem Kapitel noch beschrieben wird). Eine Untersuchung des Tokens für den Cortana-Prozess (Searchui.exe) zeigt folgende Fähigkeiten: lkd> !process 0 1 searchui.exe PROCESS ffffe1831307d080 SessionId: 1 Cid: 29d8 Peb: fb407ec000 ParentCid: 035c DeepFreeze DirBase: 38b635000 ObjectTable: ffffcd830059e580 HandleCount: <Data Not Accessible> Image: SearchUI.exe VadRoot ffffe1831fe89130 Vads 420 Clone 0 Private 11029. Modified 2031. Locked 0. DeviceMap ffffcd82c39bc0d0 Token ffffcd82d97d18f0 ... lkd> !token ffffcd82d97d18f0 _TOKEN 0xffffcd82d97d18f0 TS Session ID: 0x1 User: S-1-5-21-3537846094-3055369412-2967912182-1001 User Groups: ... → 738 Kapitel 7 Sicherheit
Primary Group: S-1-5-21-3537846094-3055369412-2967912182-1001 Privs: 19 0x000000013 SeShutdownPrivilege Attributes - 23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default 25 0x000000019 SeUndockPrivilege Attributes - 33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes - 34 0x000000022 SeTimeZonePrivilege Attributes - Authentication ID: (0,1aa448) Impersonation Level: Anonymous TokenType: Primary Source: User32 TokenFlags: 0x4a00 ( Token in use ) Token ID: 4483430 ParentToken ID: 1aa44b Modified ID: (0, 4481b11) RestrictedSidCount: 0 RestrictedSids: 0x0000000000000000 OriginatingLogonSession: 3e7 PackageSid: S-1-15-2-1861897761-1695161497-2927542615-642690995-3278402852659745135-2630312742 CapabilityCount: 32 Capabilities: 0xffffcd82f78149b0 Capabilities: 00 S-1-15-3-1024-1216833578-114521899-3977640588-1343180512-2505059295473916851-3379430393-3088591068 Attributes - Enabled 01 S-1-15-3-1024-3299255270-1847605585-2201808924-710406709-3613095291873286183-3101090833-2655911836 Attributes - Enabled 02 S-1-15-3-1024-34359262-2669769421-2130994847-3068338639-32842714462009814230-2411358368-814686995 Attributes - Enabled 03 S-1-15-3-1 Attributes - Enabled ... 29 S-1-15-3-3633849274-1266774400-1199443125-2736873758 Attributes - Enabled 30 S-1-15-3-2569730672-1095266119-53537203-1209375796 Attributes - Enabled 31 S-1-15-3-2452736844-1257488215-2818397580-3305426111 Attributes - Enabled LowboxNumberEntry: 0xffffcd82c7539110 LowboxNumber: 2 Security Attributes: Unable to get the offset of nt!_AUTHZBASEP_SECURITY_ATTRIBUTE.ListLink Process Token TrustLevelSid: (null) → Objekte schützen Kapitel 7 739
Für Cortana sind 32 Fähigkeiten erforderlich. Das zeigt, dass dieser Prozess viel mehr Funktionen aufweist, die von den Endbenutzern akzeptiert und vom System validiert werden müssen. Den Inhalt eines Tokens können Sie sich indirekt auch auf der Registerkarte Security des Eigenschaftendialogfelds für einen Prozess in Process Explorer ansehen. Dort werden die Grup­pen und Rechte aufgeführt, die in dem Token des betreffenden Prozesses enthalten sind. Experiment: Ein Programm auf niedriger Integritätsebene starten Wenn Sie die Rechte eines Programms erhöhen – entweder durch die Option Als Administrator ausführen oder weil das Programm es verlangt –, wird es ausdrücklich auf einer höheren Integritätsebene ausgeführt. Mithilfe von PsExec von Sysinternals ist es jedoch auch möglich, ein Programm auf einer niedrigeren Integritätsebene zu starten: 1. Starten Sie den Editor mit dem folgenden Befehl auf der niedrigen Integrationsebene: c:\psexec –l notepad.exe 2. Versuchen Sie, eine Datei im Verzeichnis %SystemRoot%\System32 zu öffnen (z. B. eine der XML-Dateien). Sie werden feststellen, dass Sie das Verzeichnis durchsuchen und alle darin enthaltenen Dateien öffnen können. 3. Wählen Sie im Editor Datei > Neu. 4. Geben Sie Text in das Fenster ein und versuchen Sie, die Datei im Verzeichnis %SystemRoot%\System32 zu speichern. Dabei zeigt der Editor jedoch ein Dialogfeld an, in dem Sie auf mangelnde Berechtigungen hingewiesen werden und den Vorschlag erhalten, die Datei im Dokumentenordner zu speichern: 5. Akzeptieren Sie den Vorschlag des Editors. Dabei erhalten Sie jedoch wieder die gleiche Meldung. Das gilt auch für jeden folgenden Versuch. 6. Versuchen Sie nun, die Datei im Verzeichnis LocalLow Ihres Benutzerprofils zu speichern, das Sie in einem früheren Experiment in diesem Kapitel kennengelernt haben. Im Verzeichnis LocalLow können Sie die Datei nun endlich speichern. Das liegt daran, dass der Editor hier auf der niedrigen Integritätsebene ausgeführt wurde und LocalLow als einziges Verzeichnis ebenfalls dieser Ebene zugewiesen ist. Alle anderen Verzeichnisse, die Sie ausprobiert haben, verfügen über die implizite Integrationsebene Medium. (Das können Sie mit AccessChk überprüfen.) Das Anzeigen und Öffnen von Dateien im Verzeichnis %SystemRoot%\System32 ist jedoch trotz der impliziten mittleren Integrationsebene möglich. 740 Kapitel 7 Sicherheit
Identitätswechsel Der Identitätswechsel ist eine erfolgreiche Vorgehensweise, die im Sicherheitsmodell und im Client/Server-Programmiermodell von Windows häufig eingesetzt wird. Betrachten Sie als Beispiel eine Serveranwendung, die Ressourcen wie Dateien, Drucker oder Datenbanken bereitstellt. Clients, die auf eine dieser Ressourcen zugreifen wollen, senden eine Anforderung an den Server. Wenn der Server diese Anforderung erhält, muss er sich vergewissern, dass der Client über die Berechtigungen für den gewünschten Umgang mit der Ressource verfügt. Versucht ein Benutzer auf einem Remotecomputer etwa, eine Datei in einer NTFS-Freigabe zu löschen, muss der Server, der diese Freigabe bereitstellt, ermitteln, ob der Benutzer dazu berechtigt ist. Die offensichtliche Vorgehensweise besteht darin, den Server die Konto- und die Gruppen-SIDs des Benutzers abrufen und die Sicherheitsattribute der Datei untersuchen zu lassen. Allerdings ist dies nur sehr mühsam zu programmieren und fehleranfällig. Außerdem wäre es bei dieser Vorgehensweise nicht möglich, neue Sicherheitsmerkmale transparent zu unterstützen. Daher bietet Windows Identitätswechseldienste, um dem Server die Aufgabe zu erleichtern. Damit kann der Server dem SRM mitteilen, dass er vorübergehend das Sicherheitsprofil des Clients annimmt, der die Ressource angefordert hat. Der Server greift im Namen des Clients auf die Ressource zu und der SRM führt die Zugriffsvalidierung auf der Grundlage des angenommenen Sicherheitskontextes des Clients aus. Gewöhnlich hat ein Server Zugriff auf mehr Ressourcen als ein Client, weshalb er beim Identitätswechsel einen Teil seiner Berechtigungen verliert. Es kann jedoch auch das Gegenteil der Fall sein, nämlich dass der Server bei einem Identitätswechsel Berechtigungen gewinnt. Der Server übernimmt die Identität des Clients nur innerhalb des Threads, der die entsprechende Anforderung stellt. Die Datenstrukturen für die Threadsteuerung enthalten einen optionalen Eintrag für ein Identitätswechseltoken. Das Primärtoken des Threads, das für die echten Anmeldeinformationen des Threads steht, ist jedoch stets in der Steuerstruktur des Prozesses zugänglich. Windows ermöglicht Identitätswechsel durch verschiedene Mechanismen. Wenn ein Server mit einem Client über eine benannte Pipe kommuniziert, kann er die Windows-API-Funktion ImpersonateNamedPipeClient nutzen, um dem SRM mitzuteilen, dass er die Identität des Benutzers am anderen Ende der Pipe übernehmen möchte. Bei der Kommunikation über DDE (Dynamic Data Exchange) oder RPCs kann der Server ähnliche Anforderungen für einen Identitätswechsel mithilfe von DdeImpersonateClient bzw. RpcImpersonateClient stellen. Ein Thread kann ein Identitätswechseltoken erstellen, indem er einfach das Prozesstoken mit der Funktion ImpersonateSelf kopiert. Anschließend kann er dieses Identitätswechseltoken ändern, um etwa SIDs oder Rechte zu deaktivieren. Ein SSPI-Paket (Security Support Provider Interface) kann die Identität von Clients mit ImpersonateSecurityContext übernehmen. SSPIs implementieren ein Netzwerkauthentifizierungsprotokoll wie LAN Manager 2 oder Kerberos. Andere Schnittstellen wie COM ermöglichen Identitätswechsel mithilfe von eigenen APIs wie CoImpersonateClient. Nachdem der Serverthread seine Aufgabe erledigt hat, kehrt er zu seinem eigentlichen Sicherheitskontext zurück. Diese Art von Identitätswechsel eignet sich dafür, besondere Ak­ tionen auf Anforderung eines Clients durchzuführen und dabei die korrekte Überwachung des Objektzugriffs sicherzustellen (sodass im Überwachungsprotokoll die Identität des Clients und nicht die des Serverprozesses vermerkt wird). Der Nachteil besteht darin, dass es hiermit nicht Objekte schützen Kapitel 7 741
möglich ist, ein ganzes Programm im Kontext eines Clients auszuführen. Außerdem kann ein Identitätswechseltoken nur dann auf Dateien oder Drucker in Netzwerkfreigaben zugreifen, wenn es sich um einen Identitätswechsel auf Delegierungsebene handelt (womit wir uns in Kürze befassen werden) und die Anmeldeinformationen des Tokens für die Authentifizierung an dem Remotecomputer ausreichen oder die Datei- oder Druckerfreigabe Nullsitzungen erlaubt (also Sitzungen mit anonymer Anmeldung). Ist es erforderlich, dass eine gesamte Anwendung im Sicherheitskontext eines Clients ausgeführt wird oder ohne Identitätswechsel auf Netzwerkressourcen zugreift, dann muss der Client am System angemeldet sein. Die Windows-API-Funktion LogonUser ermöglicht dies. Sie nimmt einen Kontonamen, ein Kennwort, einen Domänen- oder Computernamen, einen Anmeldetyp (wie interaktive, Batch- oder Dienstanmeldung) sowie einen Anmeldeanbieter entgegen und gibt ein Primärtoken zurück. Ein Serverthread kann dieses Token als Identitätswechseltoken übernehmen; er kann aber auch ein Programm starten, das die Anmeldeinformationen des Clients als sein Primärtoken verwendet. Vom Standpunkt der Sicherheit aus sieht ein Programm, das mit einem Token von einer interaktiven Anmeldung mit LogonUser erstellt wird, genauso aus wie ein Programm, das sich zu Anfang interaktiv am Computer anmeldet. Der Nachteil bei dieser Vorgehensweise besteht darin, dass der Server den Kontonamen und das Kennwort des Benutzers abrufen muss. Wenn er diese Informationen über das Netzwerk überträgt, muss er sie sicher verschlüsseln, damit sie nicht von böswilligen Benutzern abgefangen werden können. Um missbräuchlichen Identitätswechseln vorzubeugen, ist es in Windows nicht zulässig, dass Server einen solchen Vorgang ohne Zustimmung des Clients vollziehen. Außerdem kann ein Clientprozess den Grad der Identitätsübernahme einschränken, den ein Serverprozess vornehmen darf, indem er bei der Verbindungsaufnahme mit dem Server eine Sicherheitsdienstqualität (Security Quality of Service, SQOS) angibt. Beispielsweise kann ein Prozess beim Öffnen einer benannten Pipe SECURITY_ANONYMOUS, SECURITY_IDENTIFICATION, SECURITY_IMPERSONATION oder SECURITY_DELEGATION als Option für die Windows-Funktion CreateFile festlegen. Diese Optionen gibt es auch für die zuvor erwähnten anderen Funktionen im Zusammenhang mit Identitätswechseln. Auf jedem dieser Grade kann ein Server verschiedene Arten von Operationen in Abhängigkeit vom Sicherheitskontext des Clients durchführen: QQ SecurityAnonymous Dies ist der am stärksten eingeschränkte Grad der Identitätsübernahme. Hierbei ist der Server nicht in der Lage, Informationen über die Identität des Clients abzurufen oder die Identität zu übernehmen. QQ SecurityIdentification Bei diesem Grad kann der Server Informationen über die Identität (die SIDs) und die Rechte des Clients abrufen, die Identität des Clients aber nicht übernehmen. QQ SecurityImpersonation Hierbei kann der Server Informationen über die Identität des Clients abrufen und seine Identität auf dem lokalen System übernehmen. QQ SecurityDelegation Dies ist der am wenigsten eingeschränkte Grad. Der Server kann die Identität des Clients auf dem lokalen System und auf Remotesystemen übernehmen. Andere Schnittstellen wie RPC verwenden ihre eigenen Konstanten mit ähnlichen Bedeutungen (z. B. RPC_C_IMP_LEVEL_IMPERSONATE). 742 Kapitel 7 Sicherheit
Wenn der Client keinen Grad für den Identitätswechel festlegt, wählt Windows SecurityImpersonation. Die Funktion CreateFile akzeptiert auch die Modifikatoren SECURITY_EFFECTIVE_ ONLY und SECURITY_CONTEXT_TRACKING für die Einstellungen des Identitätswechsels: QQ SECURITY_EFFECTIVE_ONLY Diese Angabe verhindert, dass ein Server Rechte oder Gruppen eines Clients aktiviert oder deaktiviert, während er dessen Identität übernimmt. QQ SECURITY_CONTEXT_TRACKING Hierbei spiegelt der Server auch alle laufenden Änderungen am Sicherheitskontext des Clients wider. Ist diese Option nicht angegeben, übernimmt er den Kontext des Clients in dem Zustand, den er zum Zeitpunkt des Identitätswechsels hat, ohne anschließende Änderungen an dem Clientkontext zu berücksichtigen. Diese Option ist nur dann gültig, wenn sich der Server- und der Clientprozess auf demselben System befinden. Um zu verhindern, dass ein Prozess mit niedriger Integritätsebene eine Benutzerschnittstelle erstellt, darüber Anmeldeinformationen eines Benutzers abfängt und anschließend das Token des Benutzers über LogonUser abruft, wird bei Identitätswechseln eine besondere Integritätsrichtlinie angewendet: Ein Thread kann die Identität eines Tokens mit einer höheren Inte­ gritätsebene als seiner eigenen nicht übernehmen. So ist es einer Anwendung mit niedriger Integritätsebene nicht möglich, ein Dialogfeld zu fälschen, das Administratoranmeldeinformationen abruft, und anschließend Prozesse mit höheren Rechten zu starten. Die Integritätsrichtlinie für Identitätswechseltokens besagt, dass die Integritätsebene des Zugriffstokens, das von LsaLogonUser zurückgegeben wird, nicht höher sein darf als diejenige des aufrufenden Prozesses. Eingeschränkte Tokens Ein eingeschränktes Token wird mithilfe der Funktion CreateRestrictedToken aus einem Primäroder Identitätswechseltoken erstellt. Es handelte sich dabei um eine Kopie seines Ursprungstokens mit den folgenden möglichen Änderungen: QQ Aus dem Rechtearray des Tokens können Rechte entfernt werden. QQ SIDs in dem Token können als reine Verweigerungseinträge gekennzeichnet werden. Jeglicher Zugriff für diese SIDs, der durch einen entsprechenden Zugriffssteuerungseintrag verweigert wird, ist tatsächlich untersagt, auch wenn er durch einen zuvor in dem Sicherheitsdeskriptor angegebenen Zugriffssteuerungseintrag für eine Gruppe, zu der die SID gehört, erlaubt ist. QQ SIDs in dem Token können als eingeschränkt gekennzeichnet werden. Sie durchlaufen den Algorithmus für die Zugriffsprüfung ein zweites Mal. Die Ergebnisse des ersten und des zweiten Durchlaufs müssen den Zugriff auf die Ressource gewähren, damit der Zugriff auf das Objekt zugelassen wird. Eingeschränkte Tokens sind praktisch, wenn eine Anwendung die Identität eines Clients auf einer weniger privilegierten Ebene übernehmen möchte. Dies geschieht hauptsächlich aus Sicherheitsgründen bei der Ausführung von nicht vertrauenswürdigem Code. Beispielsweise kann bei einem eingeschränkten Token das Recht zum Herunterfahren des Systems entfernt Objekte schützen Kapitel 7 743
werden, damit Code, der in seinem Sicherheitskontext ausgeführt wird, nicht in der Lage ist, das System neu zu starten. Gefilterte Administratortokens Wie Sie bereits gesehen haben, verwendet auch die Benutzerkontensteuerung eingeschränkte Tokens, um das gefilterte Administratortoken zu erstellen, das alle Benutzeranwendungen erben. Dieses Token weist folgende Merkmale auf: QQ Als Integritätsebene ist Medium festgelegt. QQ Die zuvor erwähnten Administrator- und administratorähnlichen SIDs werden als reine Verweigerungseinträge gekennzeichnet, um eine Sicherheitslücke zu vermeiden, wenn die Gruppe komplett entfernt werden muss. Verweigert beispielsweise die Zugriffssteuerungsliste einer Datei der Administratorgruppe den Zugriff, erlaubt ihn aber für eine andere Gruppe, zu der der Benutzer gehört, würde der Benutzer nach wie vor Zugriff haben, wenn die Administratorgruppe aus dem Token herausgenommen wird. Dadurch hätte die Standardidentität dieses Benutzers mehr Zugriff als seine Administratoridentität. QQ Alle Rechte außer Änderungsbenachrichtigung, Herunterfahren, Abdocken, Arbeitssatz vergrößern und Zeitzone werden entfernt. Experiment: Gefilterte Administratortokens einsehen Im Explorer können Sie einen Prozess mit dem Standardbenutzertoken oder dem Administratortoken ausführen. Gehen Sie dazu auf einem Computer mit aktivierter Benutzerkontensteuerung wie folgt vor: 1. Melden Sie sich mit einem Konto an, das zur Administratorgruppe gehört. 2. Öffnen Sie das Startmenü, geben Sie eingabe ein, rechtsklicken Sie auf die eingeblendete Option Eingabeaufforderung und wählen Sie Als Administrator ausführen, um eine Eingabeaufforderung mit erhöhten Rechten zu starten. 3. Starten Sie eine weitere Instanz von Cmd.exe, diesmal aber regulär, also ohne erhöhte Rechte. 4. Führen Sie Process Explorer mit erhöhten Rechten aus, öffnen Sie das Eigenschaftendialogfeld für die Prozesse der beiden Eingabeaufforderungen und klicken Sie jeweils auf die Registerkarte Security. Das Standardbenutzertoken enthält eine SID für einen reinen Verweigerungseintrag sowie die verbindliche Beschriftung Medium. Außerdem verfügt es nur über wenige Rechte. Das Eigenschaftendialogfeld auf der rechten Seite des folgenden Screenshots stammt von der Eingabeaufforderung mit Administratortoken, das auf der linken Seite von dem mit gefiltertem Administratortoken: → 744 Kapitel 7 Sicherheit
Virtuelle Dienstkonten Windows bietet eine besondere Art von Konten, die als virtuelle Dienstkonten (oder kurz virtuelle Konten) bezeichnet werden und die Isolierung und Zugriffssteuerung von Windows-Diensten mit einem Minimum an Verwaltungsaufwand verbessern. (Mehr Informationen über Windows-Dienste erhalten Sie in Kapitel 9 von Band 2.) Ohne diesen Mechanismus müssten Windows-Dienste unter einem der für die integrierten Dienste definierten Konten (wie Lokaler Dienst oder Netzwerkdienst) oder unter einem regulären Domänenkonto laufen. Konten wie Lokaler Dienst werden jedoch von vielen Diensten gemeinsam genutzt und bieten daher keine besonders detaillierte Rechte- und Zugriffssteuerung. Außerdem lassen sie sich nicht domänenweit verwalten. Bei Domänenkonten ist es aus Sicherheitsgründen erforderlich, regelmäßig das Kennwort zu ändern, was die Verfügbarkeit der Dienste während eines solchen Wechsels beeinträchtigen kann. Außerdem sollte jeder Dienst zur bestmöglichen Isolierung eigentlich unter seinem eigenen Konto laufen, was aber bei regulären Konten einen erheblichen Verwaltungsaufwand hervorrufen würde. Bei der Verwendung virtueller Dienstkonten läuft jeder Dienst unter seinem eigenen Konto mit seiner eigenen SID. Der Name des Kontos lautet stets NT-DIENST\, gefolgt vom internen Namen des Dienstes. Virtuelle Dienstkonten können wie alle anderen Konten in Zugriffssteuerungslisten verwendet und über Gruppenrichtlinien mit Rechten versehen werden. Sie lassen sich jedoch mit den üblichen Werkzeugen zur Kontoverwaltung nicht erstellen, löschen und Gruppen zuweisen. Objekte schützen Kapitel 7 745
Die Kennwörter für virtuelle Dienstkonten werden von Windows automatisch eingerichtet und regelmäßig geändert. Ähnlich wie Lokales System und andere Dienstkonten haben sie ein Kennwort, das den Systemadministratoren aber unbekannt ist. Experiment: Virtuelle Dienstkonten verwenden Mit dem Hilfsprogramm Service Control (Sc.exe) können Sie einen Dienst erstellen, der unter einem virtuellen Dienstkonto läuft. Gehen Sie dazu wie folgt vor: 1. Starten Sie an einer Eingabeaufforderung mit erhöhten Rechten das Befehlszeilenwerkzeug Sc.exe und geben Sie darin create ein, um einen Dienst sowie ein virtuelles Konto zu erstellen, unter dem er ausgeführt werden soll. In diesem Beispiel verwenden wir den Dienst srvany aus dem Windows 2003 Resource Kit, das Sie von https:// www.microsoft.com/en-us/download/details.aspx?id=17657 herunterladen können. C:\Windows\system32>sc create srvany obj= "NT SERVICE\srvany" binPath="c:\ temp\srvany.exe" [SC] CreateService SUCCESS 2. Der vorstehende Befehl erstellt den Dienst (in der Registrierung und in der internen Liste des Dienststeuerungs-Managers) und das virtuelle Dienstkonto. Starten Sie jetzt das MMC-Snap-In Dienste (Services.msc), markieren Sie den neuen Dienst und öffnen Sie sein Eigenschaftendialogfeld. → 746 Kapitel 7 Sicherheit
3. Rufen Sie die Registerkarte Anmelden auf. 4. Im Eigenschaftendialogfeld eines bestehenden Dienstes können Sie ein virtuelles Dienstkonto dafür erstellen. Ändern Sie dazu den Kontonamen im Feld Dieses Konto in NT-DIENST\Dienstname und leeren Sie beide Kennwortfelder. Beachten Sie aber, dass bestehende Dienste möglicherweise nicht korrekt unter einem virtuellen Dienstkonto laufen, da dieses Konto unter Umständen keinen Zugriff auf Dateien oder andere Ressourcen hat, die der Dienst braucht. → Objekte schützen Kapitel 7 747
5. In Process Explorer können Sie auf der Registerkarte Security des Eigenschaftsdialogfelds für einen Dienst, der ein virtuelles Konto verwendet, den Namen dieses virtuellen Kontos und seine Sicherheitskennung (SID) sehen. Geben Sie dazu im Eigenschaftendialogfeld des Dienstes srvany das Befehlszeilenargument notepad.exe ein. (Mit srvany können Sie normale ausführbare Dateien in Dienste verwandeln, weshalb er an der Befehlszeile den Namen einer ausführbaren Datei entgegennimmt.) Klicken Sie anschließend auf Start, um den Dienst zu starten. → 748 Kapitel 7 Sicherheit
6. Das virtuelle Dienstkonto kann in den Zugriffssteuerungseinträgen jeglicher Objekte (z. B. Dateien) verwendet werden, auf die der Dienst zugreifen muss. Wenn Sie die Registerkarte Security im Eigenschaftendialogfeld für eine Datei aufrufen und eine Zugriffssteuerungsliste erstellen, die auf das virtuelle Dienstkonto verweist, werden Sie feststellen, dass die Funktionen zur Namensüberprüfung den von Ihnen eingegebenen Kontonamen (z. B. NT-DIENST\srvany) in den Dienstnamen umgewandelt hat (srvany). In dieser abgekürzten Form erscheint er auch in der Zugriffssteuerungsliste. → Objekte schützen Kapitel 7 749
7. Mithilfe von Gruppenrichtlinien können Sie dem virtuellen Dienstkonto Berechtigungen (oder Benutzerrechte) zuweisen. In diesem Beispiel hat der Dienst srvany das Recht erhalten, eine Auslagerungsdatei zu erstellen. Dazu wurde der Editor für lokale Sicherheitsrichtlinien (Secpol.msc) verwendet. → 750 Kapitel 7 Sicherheit
8. In Benutzerverwaltungswerkzeugen wie Iusrmgr.msc wird das virtuelle Dienstkonto nicht angezeigt, da es nicht im Registrierungshive SAM gespeichert ist. Wenn Sie sich die Registrierung aber (wie zuvor beschrieben) im Kontext des integrierten Systemkontos ansehen, finden Sie Belege für das Vorhandensein dieses Kontos im Schlüssel HKLM\ Security\Policy\Secrets: C:\>psexec –s –i –d regedit.exe Sicherheitsdeskriptoren und Zugriffssteuerung Tokens, die die Anmeldeinformationen eines Benutzers angeben, sind nur ein Teil der Gleichung für die Objektsicherheit. Ein weiterer Teil besteht aus den Sicherheitsinformationen, die mit einem Objekt verknüpft sind und angeben, wer welche Aktionen an dem Objekt ausführen kann. Die Datenstruktur für diese Information wird als Sicherheitsdeskriptor bezeichnet. Er besteht aus den folgenden Attributen: QQ Revisionsnummer Dies ist die Versionsnummer des SRM-Sicherheitsmodells, das zum Erstellen des Deskriptors verwendet wurde. QQ Flags Diese optionalen Modifizierer legen das Verhalten oder die Eigenschaften des Deskriptors fest. Sie sind in Tabelle 7–5 aufgeführt (und die meisten von ihnen sind im Windows SDK dokumentiert). QQ Besitzer-SID QQ Gruppen-SID Die SID der primären Gruppe des Objekts. Dieses Attribut wurde vom POSIX-Teilsystem verwendet. Da POSIX nicht mehr unterstützt wird, ist es nicht länger in Gebrauch. Objekte schützen Kapitel 7 751
QQ Benutzerverwaltete Zugriffssteuerungsliste (Discretionary Access Control List, DACL) Gibt an, wer welchen Zugriff auf das Objekt hat. QQ Systemzugriffssteuerungsliste (System Access Control List, SACL) Gibt an, welche Operationen welcher Benutzer im Systemüberwachungsprotokoll festgehalten werden sollen, und legt die ausdrückliche Integritätsebene des Objekts fest. Flag Bedeutung SE_OWNER_DEFAULTED Kennzeichnet einen Sicherheitsdeskriptor mit einer standardmäßigen Besitzer-SID. Verwenden Sie dieses Bit, um alle Objekte zu finden, die über standardmäßige Besitzerberechtigungen verfügen. SE_GROUP_DEFAULTED Kennzeichnet einen Sicherheitsdeskriptor mit einer standardmäßigen Gruppen-SID. Verwenden Sie dieses Bit, um alle Objekte zu finden, die über standardmäßige Gruppenberechtigungen verfügen. SE_DACL_PRESENT Kennzeichnet einen Sicherheitsdeskriptor mit einer DACL. Wenn dieses Flag nicht gesetzt ist oder wenn die DACL NULL ist, gewährt der Sicher­ heitsdeskriptor Vollzugriff für alle. SE_DACL_DEFAULTED Kennzeichnet einen Sicherheitsdeskriptor mit einer Standard-DACL. Wenn ein Objektersteller keine DACL angibt, erhält das Objekt die Standard-DACL vom Zugriffstoken des Erstellers. Dieses Flag wirkt sich darauf aus, wie das System die DACL im Hinblick auf die Vererbung von Zugriffssteuerungseinträgen behandelt. Wenn SE_DACL_PRESENT nicht gesetzt ist, wird dieses Flag ignoriert. SE_SACL_PRESENT Kennzeichnet einen Sicherheitsdeskriptor mit einer SACL. SE_SACL_DEFAULTED Kennzeichnet einen Sicherheitsdeskriptor mit einer Standard-SACL. Wenn ein Objektersteller keine SACL angibt, erhält das Objekt die Standard-SACL vom Zugriffstoken des Erstellers. Dieses Flag wirkt sich darauf aus, wie das System die SACL im Hinblick auf die Vererbung von Zugriffssteuerungseinträgen behandelt. Wenn SE_SACL_PRESENT nicht gesetzt ist, wird dieses Flag ignoriert. SE_DACL_UNTRUSTED Gibt an, dass die Zugriffssteuerungsliste, auf die die DACL des Sicherheitsdeskriptors zeigt, aus einer nicht vertrauenswürdigen Quelle stammt. Wenn dieses Flag gesetzt ist, ersetzt das System Server-SIDs in zusammengesetzten Zugriffssteuerungseinträgen durch bekannte gültige SIDs. SE_SERVER_SECURITY Verlangt, dass der Anbieter für das von dem Sicherheitsdeskriptor geschützte Objekt eine Serverzugriffssteuerungsliste auf der Grundlage der Zugriffssteuerungsliste in der Eingabe sein muss, und zwar unabhängig von ihrer Quelle (explizit angegebene oder Standardzugriffssteuerungsliste). Dazu werden alle GRANT-Zugriffssteuerungseinträge durch zusammengesetzte Einträge ersetzt, die dem aktuellen Server Zugriff gewähren. Dieses Flag ist nur dann sinnvoll, wenn das Subjekt einen Identitätswechsel vornimmt. SE_DACL_AUTO_INHERIT_REQ Verlangt, dass der Anbieter für das von dem Sicherheitsdeskriptor geschützte Objekt die DACL automatisch auf die Kindobjekte überträgt. Wenn der Anbieter eine automatische Vererbung unterstützt, wird die DACL auf alle vorhandenen Kindobjekte übertragen und das Bit SE_DACL_AUTO_INHERITED in den Sicherheitsdeskriptoren des Elternund des Kindobjekts gesetzt. SE_SACL_AUTO_INHERIT_REQ Verlangt, dass der Anbieter für das von dem Sicherheitsdeskriptor geschützte Objekt die SACL automatisch auf die Kindobjekte überträgt. Wenn der Anbieter eine automatische Vererbung unterstützt, wird die SACL auf alle vorhandenen Kindobjekte übertragen und das Bit SE_SACL_AUTO_INHERITED in den Sicherheitsdeskriptoren des Elternund des Kindobjekts gesetzt. 752 Kapitel 7 Sicherheit →
Flag Bedeutung SE_DACL_AUTO_INHERITED Kennzeichnet einen Sicherheitsdeskriptor, in dem die DACL für die automatische Übertragung vererbbarer Zugriffssteuerungseinträge an vorhandene Kindobjekte eingerichtet ist. Das System setzt dieses Bit, wenn es den Algorithmus für die automatische Vererbung an dem Objekt und seinen vorhandenen Kindobjekten ausführt. SE_SACL_AUTO_INHERITED Kennzeichnet einen Sicherheitsdeskriptor, in dem die SACL für die automatische Übertragung vererbbarer Zugriffssteuerungseinträge an vorhandene Kindobjekte eingerichtet ist. Das System setzt dieses Bit, wenn es den Algorithmus für die automatische Vererbung an dem Objekt und seinen vorhandenen Kindobjekten ausführt. SE_DACL_PROTECTED Verhindert, dass die DACL eines Sicherheitsdeskriptors durch vererbbare Zugriffssteuerungseinträge verändert wird. SE_SACL_PROTECTED Verhindert, dass die SACL eines Sicherheitsdeskriptors durch vererbbare Zugriffssteuerungseinträge verändert wird. SE_RM_CONTROL_VALID Gibt an, dass die Bits für den Ressourcensteuerungs-Manager in dem Sicherheitsdeskriptor gültig sind. Dabei handelt es sich um acht Bits in der Struktur des Sicherheitsdeskriptors mit Informationen für den Ressourcen-Manager, der auf die Struktur zugreift. SE_SELF_RELATIVE Kennzeichnet einen Sicherheitsdeskriptor in einem selbstbezüglichen Format, bei dem sich alle Sicherheitsinformationen in einem zusammenhängenden Speicherblock befinden. Wenn dieses Flag nicht gesetzt ist, weist der Sicherheitsdeskriptor ein absolutes Format auf. Tabelle 7–5 Flags für Sicherheitsdeskriptoren Sicherheitsdeskriptoren lassen sich in Programmen mit verschiedenen Funktionen abrufen, z. B. mit GetSecurityInfo, GetKernelObjectSecurity, GetFileSecurity, GetNamedSecurityInfo und weiteren, noch esoterischeren Funktionen. Anschließend können Sie den Deskriptor bearbeiten und die zugehörige Set-Funktion aufrufen, um die Änderung vorzunehmen. Überdies bietet die Sprache SDDL (Security Descriptor Definition Language) die Möglichkeit, einen Sicherheitsdeskriptor in Form eines kompakten Strings zusammenzusetzen, den Sie dann durch einen Aufruf von ConvertStringSecurityDescriptorToSecurityDescriptor in einen echten Sicherheitsdeskriptor verwandeln können. Wie zu erwarten, gibt es auch die Umkehrfunktion ConvertSecurityDescriptorToStringSecurityDescriptor. Eine ausführliche Beschreibung der SDDL finden Sie im Windows SDK. Eine Zugriffssteuerungsliste besteht aus einem Header und null oder mehr Strukturen für Zugriffssteuerungseinträge (Access Control Entries, ACEs). Es gibt zwei Arten von Zugriffssteuerungslisten, DACLs und SACLs. In einer DACL enthält jeder Zugriffssteuerungseintrag eine SID und eine Zugriffsmaske (einen Satz von Flags, wie wir in Kürze erklären werden). Diese Maske gibt die Zugriffsrechte (Lesen, Schreiben, Löschen usw.) an, die dem Halter der SID gewährt oder verweigert werden. In einer DACL können neun Arten von Zugriffssteuerungseinträgen auftreten: Zugriff gewährt, Zugriff verweigert, Objekt gewährt, Objekt verweigert, Callback gewährt, Callback verweigert, Objektcallback gewährt, Objektcallback verweigert und bedingte Ansprüche. Wie zu erwarten, erlauben Zugriffssteuerungseinträge vom Typ »Zugriff gewährt« einem Benutzer den Zugriff, während Einträge vom Typ »Zugriff verweigert« die in der Zugriffsmaske angegebenen Zugriffsrechte verweigern. Die Callbackeinträge werden von Anwendungen genutzt, die die AuthZ-API verwenden, um einen Callback zu re- Objekte schützen Kapitel 7 753
gistrieren, den AuthZ bei der Zugriffsprüfung anhand des vorliegenden Zugriffssteuerungseintrags aufruft. Der Unterschied zwischen »Zugriff gewährt/verweigert« und »Objekt gewährt/verweigert« besteht darin, dass die »Objekt«-Einträge nur in Active Directory verwendet werden. Zugriffssteuerungseinträge dieser Art weisen ein GUID-Feld auf. Es bedeutet, dass der Eintrag nur für Objekte oder Unterobjekte gilt, die über GUID, also global eindeutige Bezeichner (Globally Unique Identifier) von 128 Bit verfügen. Wenn in einem Active Directory-Container mit Zugriffssteuerungseintrag ein Kindobjekt erstellt wird, gibt ein weiterer, optionaler GUID an, welche Art von Kindobjekt den Eintrag erbt. Der Eintrag für bedingte Ansprüche ist in einer ACE-Struktur von einem der Callbacktypen gespeichert und wird im Abschnitt über AuthZ beschrieben. Die Zugriffsrechte, die durch die einzelnen Zugriffssteuerungseinträge gewährt werden, bilden zusammen die Menge der Zugriffsrechte, die die Zugriffssteuerungsliste gewährt. Wenn in einem Sicherheitsdeskriptor keine DACL angegeben ist (oder eine NULL-DACL), haben alle Vollzugriff auf das Objekt. Ist die DACL leer (enthält sie also keine Einträge), hat kein Benutzer Zugriff auf das Objekt. Für Zugriffssteuerungseinträge in DACLs gibt es auch Flags, die die Vererbung der Einträge regeln. In einigen Objektnamensräumen gibt es Container und Objekte, wobei ein Container andere Container- und Blattobjekte enthalten kann, die dann seine Kindobjekte sind. Container können beispielsweise Verzeichnisse im Namensraum des Dateisystems sein oder Schlüssel im Namensraum der Registrierung. Bestimmte Flags in einem Zugriffssteuerungseintrag legen fest, wie der Eintrag an die Kindobjekte des Containers vererbt wird, zu dem der Eintrag gehört. Tabelle 7–6, entnommen aus dem Windows SDK, führt einige ACE-Flags und die damit festgelegten Vererbungsregeln auf. Flag Vererbungsregel CONTAINER_INHERIT_ACE Kindobjekte, die selbst Container sind, z. B. Verzeichnisse, erben den Zugriffssteuerungseintrag als effektiven Zugriffssteuerungseintrag. Der geerbte Eintrag kann weitervererbt werden, es sei denn, dass das Bit NO_PROPAGATE_INHERIT_ACE ebenfalls gesetzt ist. INHERIT_ONLY_ACE Dieses Flag kennzeichnet einen reinen Vererbungs-ACE, der nicht dazu dient, den Zugriff auf das zugehörige Objekt zu steuern. INHERITED_ACE Dieses Flag gibt an, dass der Zugriffssteuerungseintrag geerbt wurde. Das System setzt dieses Bit, wenn es einen vererbbaren Eintrag auf ein Kindobjekt überträgt. NO_PROPAGATE_INHERIT_ACE Wird der Zugriffssteuerungseintrag von einem Kindobjekt geerbt, entfernt das System aus dem Eintrag die Flags OBJECT_INHERIT_ACE und CONTAINER_INHERIT_ACE. Dadurch wird verhindert, dass er an die nächste Objektgeneration weitervererbt wird. OBJECT_INHERIT_ACE Kindobjekte, die keine Container sind, erben den Zugriffssteuerungseintrag als effektiven Zugriffssteuerungseintrag. Kindobjekte, die Container sind, erben ihn dagegen als reinen Vererbungs-ACE, es sei denn, dass das Flag NO_PROPAGATE_INHERIT_ACE ebenfalls gesetzt ist. Tabelle 7–6 ACE-Flags für Vererbungsregeln 754 Kapitel 7 Sicherheit
Eine SACL enthält zwei Arten von Zugriffssteuerungseinträgen, nämlich Systemüberwachungs-ACEs und Systemüberwachungsobjekt-ACEs. Sie geben an, welche Operationen, die die angegebenen Benutzer oder Gruppen an dem Objekt ausführen, überwacht werden sollen. Die Überwachungsinformationen werden im Systemüberwachungsprotokoll gespeichert. Dabei können sowohl erfolgreiche als auch fehlgeschlagene Versuche aufgezeichnet werden. Ebenso wie die objektspezifischen DACL-Einträge weisen auch die Systemüberwachungsobjekt-ACEs einen GUID auf, der angibt, für welche Arten von Objekten oder Unterobjekten der Eintrag jeweils gilt, sowie einen optionalen GUID, der die Vererbung des Eintrags an bestimmte Arten von Kindobjekten regelt. Ist die SACL NULL, findet keine Überwachung statt. (Die Sicherheitsüberwachung wird weiter hinten in diesem Kapitel beschrieben.) Die Vererbungsflags für DACL-Einträge gelten auch für Systemüberwachungs- und Systemüberwachungsobjekt-ACEs. Abb. 7–8 gibt eine vereinfachte Darstellung eines Dateiobjekts und seiner DACL. Der erste Zugriffssteuerungseintrag erlaubt dem Benutzer USER1, die Datei zu lesen, der zweite verweigert den Mitgliedern der Gruppe TEAM1 den Zugriff auf die Datei und der dritte gewährt allen Benutzern (Jeder) Ausführungszugriff. Dateiobjekt Sicherheits­ deskriptor Zulassen USER1 Daten lesen Verweigern TEAM1 Daten schreiben Zulassen Jeder Datei ausführen Abbildung 7–8 Besitzerverwaltete Zugriffssteuerungsliste (DACL) Experiment: Einen Sicherheitsdeskriptor einsehen Die meisten Teilsysteme der Exekutive verlassen sich zur Verwaltung der Sicherheitsdeskriptoren für ihre Objekte auf die regulären Sicherheitsfunktionen des Objekt-Managers. Um die Sicherheitsdeskriptoren für diese Objekte zu speichern, nutzen diese Funktionen einen entsprechenden Zeiger. Beispielsweise hinterlegt der Objekt-Manager in den Headern von Prozess- und Threadobjekten Verweise auf die Sicherheitsdeskriptoren der Prozesse bzw. Threads. Auch in den entsprechenden Zeigern von Ereignissen, Mutexen und Semaphoren werden Verweise auf deren Sicherheitsdeskriptoren gespeichert. Mit einem Live-Kerneldebugging können Sie sich wie folgt die Sicherheitsdeskriptoren dieser Objekte ansehen. Dazu müssen Sie als Erstes deren Header ausfindig machen. Sicherheitsdeskriptoren für Prozesse können Sie sich auch in Process Explorer und mit AccessChk anzeigen lassen. → Objekte schützen Kapitel 7 755
1. Beginnen Sie mit dem lokalen Kerneldebugger. 2. Geben Sie !process 0 0 explorer.exe ein, um Informationen über den Explorer-Prozess abzurufen: lkd> !process 0 0 explorer.exe PROCESS ffffe18304dfd780 SessionId: 1 Cid: 23e4 Peb: 00c2a000 ParentCid: 2264 DirBase: 2aa0f6000 ObjectTable: ffffcd82c72fcd80 HandleCount: <Data Not Accessible> Image: explorer.exe PROCESS ffffe1830670a080 SessionId: 1 Cid: 27b8 Peb: 00950000 ParentCid: 035c DirBase: 2cba97000 ObjectTable: ffffcd82c7ccc500 HandleCount: <Data Not Accessible> Image: explorer.exe 3. Wird mehr als eine Instanz des Explorers angezeigt, wählen Sie willkürlich eine davon aus. Geben Sie !object mit der Adresse der EPROCESS-Struktur aus der Ausgabe des vorherigen Befehls als Argument ein, um sich die Objektdatenstruktur anzeigen zu lassen: lkd> !object ffffe18304dfd780 Object: ffffe18304dfd780 Type: (ffffe182f7496690) Process ObjectHeader: ffffe18304dfd750 (new version) HandleCount: 15 PointerCount: 504639 4. Geben Sie dt _OBJECT_HEADER und die Adresse des Objektheaderfelds aus der Ausgabe des vorherigen Befehls ein, um sich die Datenstruktur des Objektheaders anzusehen. Darunter befindet sich auch der Zeiger auf den Sicherheitsdeskriptor: lkd> dt nt!_object_header ffffe18304dfd750 +0x000 PointerCount : 0n504448 +0x008 HandleCount : 0n15 +0x008 NextToFree : 0x00000000'0000000f Void +0x010 Lock : _EX_PUSH_LOCK +0x018 TypeIndex : 0xe5 '' +0x019 TraceFlags : 0 '' +0x019 DbgRefTrace : 0y0 +0x019 DbgTracePermanent : 0y0 +0x01a InfoMask : 0x88 '' +0x01b Flags : 0 '' +0x01b NewObject : 0y0 +0x01b KernelObject : 0y0 +0x01b KernelOnlyAccess : 0y0 +0x01b ExclusiveObject : 0y0 → 756 Kapitel 7 Sicherheit
+0x01b PermanentObject : 0y0 +0x01b DefaultSecurityQuota : 0y0 +0x01b SingleHandleEntry : 0y0 +0x01b DeletedInline : 0y0 +0x01c Reserved : 0x30003100 +0x020 ObjectCreateInfo : 0xffffe183'09e84ac0 _OBJECT_CREATE_INFORMATION +0x020 QuotaBlockCharged : 0xffffe183'09e84ac0 Void +0x028 SecurityDescriptor : 0xffffcd82'cd0e97ed Void +0x030 Body 5. : _QUAD Jetzt können Sie den Sicherheitsdeskriptor mit dem Debuggerbefehl !sd abrufen. Im Objektheader werden in dem Zeiger auf den Sicherheitsdeskriptor einige der niederwertigen Bits als Flags verwendet, weshalb Sie sie erst auf null setzen müssen, bevor Sie dem Zeiger folgen können. Auf 32-Bit-Systemen gibt es drei Flagbits. Daher müssen Sie auf die in der Objektheaderstruktur angegebene Deskriptoradresse noch & -8 anwenden. Auf 64-Bit-Systemen mit vier Flagbits verwenden Sie stattdessen & -10. lkd> !sd 0xffffcd82'cd0e97ed & -10 ->Revision: 0x1 ->Sbz1 : 0x0 ->Control : 0x8814 SE_DACL_PRESENT SE_SACL_PRESENT SE_SACL_AUTO_INHERITED SE_SELF_RELATIVE ->Owner : S-1-5-21-3537846094-3055369412-2967912182-1001 ->Group : S-1-5-21-3537846094-3055369412-2967912182-1001 ->Dacl : ->Dacl : ->AclRevision: 0x2 ->Dacl : ->Sbz1 : 0x0 ->Dacl : ->AclSize : 0x5c ->Dacl : ->AceCount : 0x3 ->Dacl : ->Sbz2 : 0x0 ->Dacl : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE ->Dacl : ->Ace[0]: ->AceFlags: 0x0 ->Dacl : ->Ace[0]: ->AceSize: 0x24 ->Dacl : ->Ace[0]: ->Mask : 0x001fffff ->Dacl : ->Ace[0]: ->SID: S-1-5-21-3537846094-3055369412-2967912182-1001 ->Dacl : ->Ace[1]: ->AceType: ACCESS_ALLOWED_ACE_TYPE ->Dacl : ->Ace[1]: ->AceFlags: 0x0 ->Dacl : ->Ace[1]: ->AceSize: 0x14 ->Dacl : ->Ace[1]: ->Mask : 0x001fffff → Objekte schützen Kapitel 7 757
->Dacl : ->Ace[1]: ->SID: S-1-5-18 ->Dacl : ->Ace[2]: ->AceType: ACCESS_ALLOWED_ACE_TYPE ->Dacl : ->Ace[2]: ->AceFlags: 0x0 ->Dacl : ->Ace[2]: ->AceSize: 0x1c ->Dacl : ->Ace[2]: ->Mask : 0x00121411 ->Dacl : ->Ace[2]: ->SID: S-1-5-5-0-1745560 ->Sacl : ->Sacl : ->AclRevision: 0x2 ->Sacl : ->Sbz1 : 0x0 ->Sacl : ->AclSize : 0x1c ->Sacl : ->AceCount : 0x1 ->Sacl : ->Sbz2 : 0x0 ->Sacl : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE ->Sacl : ->Ace[0]: ->AceFlags: 0x0 ->Sacl : ->Ace[0]: ->AceSize: 0x14 ->Sacl : ->Ace[0]: ->Mask : 0x00000003 ->Sacl : ->Ace[0]: ->SID: S-1-16-8192 Der Sicherheitsdeskriptor enthält drei Zugriffssteuerungseinträge vom Typ »Zugriff gewährt«: einen für den aktuellen Benutzer (S-1-5-21-3537846094-3055369412-29679121821001), einen für das Systemkonto (S-1-5-18) und einen dritten für die Anmelde-SID (S-1-55-0-1745560). Die Systemzugriffssteuerungsliste hat nur einen Eintrag (S-1-16-8192). Zuweisung von Zugriffssteuerungslisten Um zu bestimmen, welche DACL es einem neuen Objekt zuweisen soll, wendet das Sicherheitssystem die erste der folgenden vier Regeln an, die in dem jeweiligen Fall geeignet ist: 1. Wenn der Aufrufer beim Erstellen des Objekts ausdrücklich einen Sicherheitsdeskriptor angibt, wendet das Sicherheitssystem diesen auf das Objekt an. Hat das Objekt einen Namen und befindet es sich in einem Containerobjekt (z. B. ein benanntes Ereignisobjekt im Verzeichnis \BaseNamedObjects des Objekt-Manager-Namensraums), führt das System alle vererbbaren Zugriffssteuerungseinträge (also solche, die vom Container des Objekts übernommen werden können) in der DACL zusammen, es sei denn, dass das Flag SE_DACL_­ PROTECTED gesetzt ist, das eine Vererbung verhindert. 2. Wenn der Aufrufer keinen Sicherheitsdeskriptor angegeben hat, das Objekt aber benannt ist, schaut sich das Sicherheitssystem den Sicherheitsdeskriptor des Containers an, in dem der neue Objektname gespeichert ist. Einige der Zugriffssteuerungseinträge für das Objektverzeichnis können als vererbbar gekennzeichnet sein, was bedeutet, dass sie auf neue, in diesem Verzeichnis erstellte Objekte angewendet werden sollten. Sind solche vererbbaren Einträge vorhanden, bildet das Sicherheitssystem daraus eine Zugriffssteuerungsliste, die es an das neue Objekt anhängt. (Manche Einträge können durch besondere Flags auch 758 Kapitel 7 Sicherheit
so gekennzeichnet sein, dass sie nur von Containerobjekten und nicht von anderen Arten von Objekten geerbt werden können.) 3. Wenn kein Sicherheitsdeskriptor angegeben ist und das Objekt keine Zugriffssteuerungseinträge erbt, ruft das Sicherheitssystem die Standard-DACL vom Zugriffstoken des Aufrufers ab und wendet sie auf das neue Objekt an. Mehrere Teilsysteme von Windows verfügen über hartcodierte DACLs, die sie beim Erstellen eines Objekts zuweisen (z. B. Dienste, LSA- und SAM-Objekte). 4. Wenn kein Sicherheitsdeskriptor angegeben ist, keine Zugriffssteuerungseinträge geerbt werden und es auch keine Standard-DACL gibt, erstellt das System das Objekt ohne DACL. Dadurch hat Jeder (also alle Benutzer und Gruppen) Vollzugriff auf das Objekt. Die dritte Regel ist mit dieser identisch, wenn das Token eine NULL-Standard-DACL angibt. Bei der Zuweisung einer SACL zu einem neuen Objekt wendet das System ähnliche Regeln wie für die DACL an, allerdings mit folgenden Ausnahmen: QQ Geerbte Systemüberwachungs-ACEs werden nicht an Objekte weitervererbt, deren Sicherheitsdeskriptoren das Flag SE_SACL_PROTECTED aufweisen (vergleichbar mit dem Flag SE_DACL_PROTECTED zum Schutz von DACLs). QQ Sind keine Sicherheitsüberwachungs-ACEs angegeben und wird keine SACL geerbt, so wird auf das Objekt keine SACL angewendet. Dieses Verhalten unterscheidet sich von dem bei DACL, bei denen im vergleichbaren Fall eine Standard-DACL verwendet wird. Das liegt daran, dass es eine Standard-SACL nicht gibt. Wird auf einen Container ein neuer Sicherheitsdeskriptor angewendet, der vererbbare Zugriffssteuerungseinträge enthält, so überträgt das System diese Einträge automatisch auf die Sicherheitsdeskriptoren der Kindobjekte. (Allerdings werden solche geerbten Einträge nicht akzeptiert, wenn das Flag SE_DACL_PROTECTED bzw. SE_SACL_PROTECTED gesetzt ist.) Dabei werden die vererbbaren Einträge so mit dem vorhandenen Sicherheitsdeskriptor des Kindobjekts zusammengeführt, dass die ausdrücklich auf die Zugriffssteuerungsliste angewendeten Einträge vor den geerbten stehen. Zur Vererbung von Zugriffssteuerungseinträgen wendet das System die folgenden Regeln an: QQ Wenn ein Kindobjekt ohne DACL einen Zugriffssteuerungseintrag erbt, ergibt sich ein Kindobjekt mit einer DACL, die nur diesen geerbten Eintrag enthält. QQ Wenn ein Kindobjekt mit einer leeren DACL einen Zugriffssteuerungseintrag erbt, ergibt sich ein Kindobjekt mit einer DACL, die nur diesen geerbten Eintrag enthält. QQ Ausschließlich für Active Directory-Objekte gilt: Wenn ein vererbbarer Zugriffssteuerungseintrag von einem Objekt entfernt wird, sorgt die automatische Vererbung dafür, dass jegliche Kopien dieses Eintrags in Kindobjekten ebenfalls entfernt werden. QQ Ausschließlich für Active Directory-Objekte gilt: Werden durch die automatische Vererbung sämtliche Zugriffssteuerungseinträge aus der DACL eines Kindobjekts entfernt, so hat es anschließend eine leere DACL und nicht keine DACL. Wie Sie in Kürze sehen werden, spielt die Reihenfolge der Einträge in einer Zugriffssteuerungsliste eine wichtige Rolle im Sicherheitsmodell von Windows. Objekte schützen Kapitel 7 759
Von Objektspeichern wie Dateisystemen, der Registrierung und Active Directory wird die Vererbung im Allgemeinen nicht direkt unterstützt. Windows-APIs zur Vererbung wie SetEntriesInAcl rufen dazu entsprechende Funktionen der DLL für die Sicherheitsvererbung auf (%SystemRoot%\System32\Ntmarta.dll), die wissen, wie sie diese Objektspeicher durchlaufen müssen. Vertrauenseinträge Durch die Einführung von geschützten Prozessen und PPLs (Protected Processes Light; siehe Kapitel 3) wurde eine Vorkehrung benötigt, um dafür zu sorgen, dass Objekte nur für geschützte Prozesse zugänglich sind. Das ist wichtig, um bestimmte Ressourcen, etwa den Registrierungsschlüssel KnownDlls, gegen Manipulation zu schützen, selbst gegen Manipulation durch Code auf Administratorebene. Entsprechende Zugriffssteuerungseinträge werden mit Standard-SIDs versehen, die den erforderlichen Schutzgrad und den erforderlichen Signierer für den Zugriff angeben. Tabelle 7–7 führt dieses SIDs und ihre Bedeutung auf. SID Schutzgrad Signierer 1-19-512-0 PPL Keiner 1-19-512-4096 PPL Windows 1-19-512-8192 PPL WinTcb 1-19-1024-0 Geschützt Keiner 1-19-1024-4096 Geschützt Windows 1-19-1024-8192 Geschützt WinTcb Tabelle 7–7 Vertrauens-SIDs Eine Vertrauens-SID ist Teil eines Tokenobjekts für einen geschützten oder einen PPL-Prozess. Je höher die SID-Nummer, umso leistungsstärker ist das Token. (»Geschützt« ist ein höherer Grad als »PPL«.) 760 Kapitel 7 Sicherheit
Experiment: Vertrauens-SIDs einsehen In diesem Experiment schauen Sie sich die Vertrauens-SIDs in den Tokens von geschützten Prozessen an. 1. Starten Sie den lokalen Kerneldebugger. 2. Lassen Sie sich grundlegende Informationen über den Prozess Csrss.exe anzeigen: lkd> !process 0 1 csrss.exe PROCESS ffff8188e50b5780 SessionId: 0 Cid: 0358 Peb: b3a9f5e000 ParentCid: 02ec DirBase: 1273a3000 ObjectTable: ffffbe0d829e2040 HandleCount: <Data Not Accessible> Image: csrss.exe VadRoot ffff8188e6ccc8e0 Vads 159 Clone 0 Private 324. Modified 4470. Locked 0. DeviceMap ffffbe0d70c15620 Token ffffbe0d829e7060 ... PROCESS ffff8188e7a92080 SessionId: 1 Cid: 03d4 Peb: d5b0de4000 ParentCid: 03bc DirBase: 162d93000 ObjectTable: ffffbe0d8362d7c0 HandleCount: <Data Not Accessible>Modified 462372. Locked 0. DeviceMap ffffbe0d70c15620 Token ffffbe0d8362d060 ... 3. Wählen Sie eines der Tokens aus und lassen Sie sich die Einzelheiten anzeigen: lkd> !token ffffbe0d829e7060 _TOKEN 0xffffbe0d829e7060 TS Session ID: 0 User: S-1-5-18 ... Process Token TrustLevelSid: S-1-19-512-8192 Wie Sie sehen, handelt es sich um einen PPL mit dem Signierer WinTcb. Den Zugriff bestimmen Um den Zugriff auf ein Objekt zu bestimmen, werden die beiden folgenden Methoden verwendet: QQ Die verbindliche Integritätsprüfung, die anhand der Integritätsebene der Ressource und der erforderlichen Richtlinie ermittelt, ob die Integritätsebene des Aufrufers für den Zugriff auf die Ressource ausreicht. Objekte schützen Kapitel 7 761
QQ Die Prüfung des besitzerverwalteten Zugriffs, die den zulässigen Zugriff eines gegebenen Benutzerkontos auf ein Objekt ermittelt. Wenn ein Prozess versucht, ein Objekt zu öffnen, findet die Integritätsprüfung vor der regulären Windows-DACL-Prüfung in der Kernelfunktion SeAccessCheck statt, da sie schneller abläuft und damit rasch bestimmt werden kann, ob die vollständige Prüfung des besitzerverwalteten Zugriffs überhaupt nötig ist. Ein Prozess mit den Standardintegritätsrichtlinien in seinem Zugriffstoken (wie zuvor beschrieben TOKEN_MANDATORY_NO_WRITE_UP und TOKEN_MANDATORY_NEW_ PROCESS_MIN) kann ein Objekt für den Schreibzugriff öffnen, wenn seine Integritätsebene größer oder gleich der des Objekts ist und die DACL dem Prozess ebenfalls den gewünschten Zugriff gewährt. Ein Prozess mit niedriger Integritätsebene kann beispielsweise keinen Prozess mittlerer Integritätsebene für den Schreibzugriff öffnen, selbst wenn die DACL dies zulässt. Unter den Standardintegritätsrichtlinien können Prozesse jedes Objekt – außer Prozess-, Thread- und Tokenobjekte – für den Lesezugriff öffnen, wenn die DACL des Objekts diesen Zugriff gewährt. Ein Prozess mit niedriger Integritätsebene kann daher jegliche Dateien öffnen, die für das Benutzerkonto, unter dem er läuft, zugänglich sind. Internet Explorer im geschützten Modus verwendet Integritätsebenen, um zu verhindern, dass Malware Benutzerkontoeinstellungen ändert. Das hält die Malware jedoch nicht davon ab, in den Dokumenten der Benutzer zu lesen. Prozess-, Thread- und Tokenobjekte bilden jedoch Ausnahmen, da ihre Integritätsrichtlinie auch die Einstellung No-Read-Up enthält. Das bedeutet, dass die Integritätsebene eines Prozesses größer oder gleich der Integritätsebene des Prozesses oder Threads sein muss, den er öffnen will, und die DACL den gewünschten Zugriff erlauben muss. Tabelle 7–8 zeigt, welche Arten von Zugriff ein Prozess mit verschiedenen Integritätsebenen auf andere Prozesse und Objekte hat, sofern die DACLs diesen Zugriff ebenfalls gewährt. Zugreifender Prozess Zugriff auf Objekte Zugriff auf andere Prozesse Hohe Integritätsebene • Lese/Schreib-Zugriff auf alle Objekte mit Integritätsebene Hoch und darunter • Lesezugriff auf alle Objekte mit der Integritätsebene System • Lese/Schreib-Zugriff auf alle Prozesse mit Integritätsebene Hoch und darunter • Kein Lese/Schreib-Zugriff auf Prozesse mit der Integritätsebene System Mittlere Integritätsebene • Lese/Schreib-Zugriff auf alle Objekte mit mittlerer und niedriger Integritätsebene • Lesezugriff auf alle Objekte mit der Integritätsebene Hoch oder System • Lese/Schreib-Zugriff auf alle Prozesse mit mittlerer und niedriger Integritätsebene • Kein Lese/Schreib-Zugriff auf Prozesse mit der Integritätsebene Hoch oder System Niedrige Integritätsebene • Lese/Schreib-Zugriff auf alle Objekte mit niedriger Integritätsebene • Lesezugriff auf alle Objekte mit der Integritätsebene Mittel oder höher • Lese/Schreib-Zugriff auf alle Prozesse mit niedriger Integritätsebene • Kein Lese/Schreib-Zugriff auf Prozesse mit der Integritätsebene Mittel oder höher Tabelle 7–8 Zugriff auf Objekte und Prozesse nach Integritätsebene 762 Kapitel 7 Sicherheit
Mit dem Lesezugriff auf einen Prozess ist in diesem Abschnitt ein vollständiger Lesezugriff gemeint, also das Lesen der Inhalte im Prozessadressraum. No-ReadUp verhindert nicht, dass ein Prozess einen Prozess mit höherer Integritätsebene für beschränkten Zugriff öffnen kann, z. B. für PROCESS_QUERY_LIMITED_INFORMATION, wobei nur grundlegende Informationen über den Prozess abgefragt werden. User Interface Privilege Isolation Auch das Messaging-Teilsystem von Windows nutzt Integritätsebenen, um eine Trennung der Rechte für die Benutzerschnittstelle (User Interface Privilege Isolation, UIPI) zu erreichen. Dabei hindert das Teilsystem Prozesse daran, Fensternachrichten an Fenster zu senden, die Prozessen mit einer höheren Integritätsebene gehören. Eine Ausnahme bilden die folgenden rein informativen Nachrichten: QQ WM_NULL QQ WM_MOVE QQ WM_SIZE QQ WM_GETTEXT QQ WM_GETTEXTLENGTH QQ WM_GETHOTKEY QQ WM_GETICON QQ WM_RENDERFORMAT QQ WM_DRAWCLIPBOARD QQ WM_CHANGECBCHAIN QQ WM_THEMECHANGED Die Beachtung der Integrationsebenen hindert Standardbenutzerprozesse daran, Eingaben in Fenstern von erhöhten Rechten vorzunehmen und Shatter-Angriffe durchzuführen (etwa dadurch, einem Prozess nicht wohlgeformte Nachrichten zu senden, die einen internen Pufferüberlauf auslösen, wodurch es möglich wäre, Code mit den erhöhten Rechten des Prozesses auszuführen). UIPI lässt auch nicht zu, dass sich Fenster-Hooks (SetWindowsHookEx) auf die Fenster von Prozessen mit höherer Integritätsebene auswirken. Dadurch ist es beispielsweise nicht möglich, dass ein Standardbenutzerprozess die Tastatureingaben eines Benutzers in einer Administratoranwendung aufzeichnet. Auch Journal-Hooks werden blockiert, um zu verhindern, dass Prozesse das Verhalten von Prozessen einer höheren Integritätsebene überwachen. → Objekte schützen Kapitel 7 763
Prozesse auf der mittleren und einer höheren Integritätsebene können die API ChangeWindowsMessageFilterEx aufrufen, um zusätzliche Nachrichten durchzulassen. Das wird gewöhnlich getan, um Nachrichten zu erlauben, die benutzerdefinierte Steuerelemente für die Kommunikation außerhalb der Standardsteuerelemente von Windows benötigen. Die ältere API ChangeWindowMessageFilter leistet Ähnliches, wirkt sich aber auf einen ganzen Prozess aus und nicht nur auf ein bestimmtes Fenster. Bei der Verwendung von ChangeWindowMessageFilter können zwei benutzerdefinierte Steuerelemente innerhalb eines Prozesses die gleichen internen Fensternachrichten nutzen. Dabei kann es passieren, dass eine möglicherweise schädliche Fensternachricht des einen Steuerelements durchgelassen wird, wenn es sich bei ihm lediglich um eine reine Abfragenachricht für das andere Steuerelement handelt. Da Bedienhilfen wie die Bildschirmtastatur (Osk.exe) den UIPI-Einschränkungen unterliegen (die es erfordern würden, dass die Bedienhilfe für alle Integritätsebenen der auf dem Desktop sichtbaren Prozesse ausgeführt wird), können die Prozesse dieser Anwendungen den Zugriff auf die Benutzerschnittstelle aktivieren. Das entsprechende Flag kann in der Manifestdatei des Abbilds vorhanden sein und führt den Prozess auf einer leicht höheren Integritätsebene als der mittleren aus (zwischen 0x2000 und 0x3000), wenn sie von einem Standardbenutzerkonto gestartet wurde, bzw. auf der hohen Integritätsebene, wenn sie von einem Administratorkonto gestartet wurde. Im zweiten Fall wird die Anforderung zur Rechteerhöhung nicht angezeigt. Damit ein Prozess dieses Flag setzen kann, muss das Abbild signiert sein und sich in einem der verschiedenen sicheren Speicherorte befinden, z. B. %SystemRoot% oder %ProgramFiles%. Wenn die Integritätsprüfung abgeschlossen ist und die erforderliche Richtlinie den Zugriff auf das Objekt erlaubt, wird einer der beiden folgenden Algorithmen verwendet, um den besitzerverwalteten Zugriff auf das Objekt zu bestimmen. Das Resultat dieses Algorithmus bestimmt dann das Gesamtergebnis der Zugriffsprüfung. QQ Der maximal mögliche Zugriff auf das Objekt wird bestimmt. Er kann in gewisser Form mithilfe der AuthZ-API (siehe den eigenen Abschnitt dazu weiter hinten in diesem Kapitel) oder der älteren Funktion GetEffectiveRightsFromAcl in den Benutzermodus exportiert werden. Diese Vorgehensweise wird auch angewendet, wenn ein Programm einen Zugriff vom Typ MAXIMUM_ALLOWED anfordert, was ältere APIs tun, die keinen Parameter für den gewünschten Zugriff haben. QQ Es wird ermittelt, ob der gewünschte Zugriff zulässig ist. Dies kann mithilfe der Windows-​ Funktionen AccessCheck und AccessCheckByType geschehen. Der erste Algorithmus untersucht die Einträge in der DACL wie folgt: 1. 764 Wenn das Objekt keine DACL (eine NULL-DACL) hat, ist es nicht geschützt. Das Sicherheitssystem gewährt jeglichen Zugriff bis auf den von einem AppContainer-Prozess (siehe den Abschnitt »Anwendungscontainer« weiter hinten in diesem Kapitel). In letzterem Fall wird der Zugriff verweigert. Kapitel 7 Sicherheit
2. Wenn der Aufrufer das Recht »Besitz übernehmen« hat, gewährt das System vor der Untersuchung einen Zugriff vom Typ »Besitzer festlegen«. 3. Wenn der Aufrufer der Besitzer des Objekts ist, sucht das System nach einer OWNER_RIGHTSSID und verwendet diese für die nächsten Schritte. Anderenfalls werden die Zugriffsrechte »Lesezugriff« und »DACL schreiben« gewährt. 4. Die Zugriffsmaske jedes Verweigerungseintrags mit einer SID, die derjenigen im Zugriffs­ token des Aufrufers entspricht, wird von der Zulassungsmaske entfernt. 5. Die Zugriffsmaske jedes Zulassungseintrags mit einer SID, die derjenigen im Zugriffstoken des Aufrufers entspricht, wird der zu berechnenden Zulassungsmaske hinzugefügt, es sei denn, dass der betreffende Zugriff bereits verweigert wurde. Nach der Untersuchung aller Einträge in der DACL wird die berechnete Zulassungsmaske als »maximal möglicher Zugriff« an den Aufrufer zurückgegeben. Diese Maske gibt sämtliche Arten des Zugriffs an, die der Aufrufer beim Öffnen des Objekts erfolgreich anfordern kann. Diese Beschreibung gilt jedoch nur für die Kernelmodusform des Algorithmus. Die in GetEffectiveRightsFromAcl implementierte Version führt Schritt 2 nicht aus und betrachtet statt eines Zugriffstokens eine einzelne Benutzer- oder Gruppen-SID. Besitzerrechte Da Objektbesitzer stets die Rechte »Lesezugriff« und »DACL schreiben« für ihr Objekt haben und damit in der Lage sind, dessen Sicherheitseinstellungen zu überschreiben, gibt es in Windows eine besondere Einrichtung, um dieses Verhalten zu steuern, nämlich die Besitzerrechte-SID. Es gibt sie aus den beiden folgenden Hauptgründen: QQ Höhere Sicherheit für Dienste im Betriebssystem Wenn ein Dienst zur Laufzeit ein Objekt erstellt, wird mit diesem nicht die tatsächliche Dienst-SID als Besitzer-SID verknüpft, sondern das Konto, unter dem der Dienst läuft (also etwa das Konto des lokalen Systems oder des lokalen Dienstes). Dadurch wäre jeder andere Dienst in diesem Konto ebenfalls ein Besitzer und hätte Zugriff auf das Objekt. Dieses unerwünschte Verhalten wird durch die Benutzerrechte-SID verhindert. QQ Mehr Flexibilität für besondere Fälle Nehmen wir an, ein Administrator möchte den Benutzern erlauben, Dateien und Ordner zu erstellen, aber nicht, die Zugriffssteuerungslisten für diese Objekte zu ändern (da die Benutzer sonst versehentlich oder arglistig unerwünschten Konten Zugriff auf diese Dateien oder Ordner gewähren könnten). Durch die Verwendung einer vererbbaren Benutzerrechte-SID können die Benutzer daran gehindert werden, die Zugriffssteuerungslisten der von ihnen erstellten Objekte zu bearbeiten oder auch nur einzusehen. Ein weiteres Beispiel sind Änderungen an Gruppen. Stellen Sie sich vor, ein Angestellter hat mehrere Dateien erstellt, während er zu einer vertraulichen Gruppe gehörte, und wird nun aus geschäftlichen Gründen aus dieser Gruppe herausgenommen. Da er nach wie vor ein Benutzer ist, könnte er weiterhin auf die sensiblen Dateien zugreifen. Objekte schützen Kapitel 7 765
Mit dem zweiten Algorithmus wird ermittelt, ob eine gegebene Zugriffsanforderung auf der Grundlage des Zugriffstokens des Aufrufers gewährt werden kann. Jede Windows-API-Funktion zum Öffnen sicherungsfähiger Objekte verfügt über einen Parameter, der die Maske für den gewünschten Zugriff angibt. Dies ist der letzte Bestandteil der Sicherheitsgleichung. Um zu bestimmen, ob der Aufrufer Zugriff hat, werden die folgenden Schritte ausgeführt: 1. Wenn das Objekt keine DACL (eine NULL-DACL) hat, ist es nicht geschützt. Das Sicherheitssystem gewährt den Zugriff. 2. Wenn der Aufrufer das Recht »Besitz übernehmen« hat, gewährt das System einen Zugriff vom Typ »Besitzer festlegen«, falls dieser angefordert wurde. Anschließend untersucht es die DACL, es sei denn, dass »Besitzer festlegen« der einzige angeforderte Zugriff war. Wenn das der Fall ist, wird die DACL nicht mehr untersucht. 3. Wenn der Aufrufer der Besitzer des Objekts ist, sucht das System nach einer OWNER_RIGHTSSID und verwendet diese für die nächsten Schritte. Anderenfalls werden die Zugriffsrechte »Lesezugriff« und »DACL schreiben« gewährt. Wenn das die einzigen angeforderten Zugriffsrechte waren, wird der Zugriff gewährt, ohne die DACL zu untersuchen. 4. Die einzelnen Zugriffssteuerungseinträge in der DACL werden vom ersten zum letzten untersucht. Ein Zugriffssteuerungseintrag wird verarbeitet, wenn eine der folgenden Bedingungen erfüllt ist: • Es handelt sich um einen Verweigerungseintrag und die in ihm angegebene SID entspricht einer aktivierten SID (SIDs können aktiviert und deaktiviert werden) oder einer reinen Verweigerungs-SID im Zugriffstoken des Aufrufers. • Es handelt sich um einen Zulassungseintrag und die in ihm angegebene SID entspricht einer aktivierten SID im Token des Aufrufers, die keine reine Verweigerungs-SID ist. • Es ist der zweite Durchlauf durch den Deskriptor (also derjenige für die Überprüfung eingeschränkter SIDs) und die SID in dem Eintrag entspricht einer eingeschränkten SID im Zugriffstoken des Aufrufers. • Der Eintrag ist nicht als reiner Vererbungseintrag gekennzeichnet. 5. Angeforderte Rechte, die sich in der Zugriffsmaske eines Zulassungseintrags befinden, werden gewährt. Wurden alle angeforderten Zugriffsrechte gewährt, endet die Zugriffsprüfung erfolgreich. Befinden sich dagegen irgendwelche der angeforderte Rechte in der Zugriffsmaske eines Verweigerungseintrags, wird der Zugriff abgelehnt. 6. Wenn das Ende der DACL erreicht ist und einige der angeforderten Zugriffsrechte immer noch nicht gewährt sind, wird der Zugriff verweigert. 7. Wenn der gesamte Zugriff gewährt wird, das Zugriffstoken des Aufrufers aber über mindestens eine eingeschränkte SID verfügt, durchsucht das System die DACL erneut und schaut sich dabei die Zugriffssteuerungseinträge an, deren Zugriffsmasken dem angeforderten Zugriff entsprechen und deren SIDs sich unter denen der eingeschränkten SIDs des Aufrufers befinden. Nur wenn die angeforderten Zugriffsrechte bei beiden Durchläufen durch die DACL gewährt werden, erhält der Benutzer Zugriff auf das Objekt. 766 Kapitel 7 Sicherheit
Das Verhalten beider Zugriffsvalidierungsalgorithmen hängt davon ab, wie die Zulassungs- und Verweigerungseinträge relativ zueinander angeordnet sind. Stellen Sie sich ein Objekt mit nur zwei Zugriffssteuerungseinträgen vor – einem, der dem Benutzer Vollzugriff auf das Objekt gewährt, und einem, der den Zugriff verweigert. Steht der Zulassungseintrag vor dem Verweigerungseintrag, so erhält der Benutzer Vollzugriff, doch bei umgekehrter Reihenfolge wird ihm der Zugriff nicht erlaubt. Mehrere Windows-Funktionen, darunter SetSecurityInfo und SetNamedSecurityInfo, richten Zugriffssteuerungseinträge in der bevorzugten Reihenfolge ein, in der erst die ausdrücklichen Verweigerungseinträge stehen und dann die ausdrücklichen Zulassungseinträge. Diese Funktionen werden unter anderem in den Dialogfeldern des Sicherheitseditors eingesetzt, in denen Sie die Berechtigungen für NTFS-Dateien und Registrierungsschlüssel bearbeiten können. Darüber hinaus wenden SetSecurityInfo und SetNamedSecurityInfo auch Vererbungsregeln für Zugriffssteuerungseinträge auf den Sicherheitsdeskriptor an, für den sie aufgerufen werden. Abb. 7–9 zeigt ein Beispiel der Zugriffsprüfung, das die Wichtigkeit der Reihenfolge von Zugriffssteuerungseinträgen herausstreicht. Hier wird einem Benutzer, der eine Datei öffnen möchte, der Zugriff verweigert, obwohl es in der DACL des Objekts einen Eintrag gibt, der den Zugriff erlaubt. Das liegt daran, dass der Eintrag, der den Zugriff für diesen Benutzer (aufgrund seiner Mitgliedschaft in der Gruppe Autoren) verweigert, vor dem Zulassungseintrag steht. Zugriffstoken Benutzer: DaveC Gruppe 1: Administratoren Gruppe 2: Autoren Zugriff zum Öffnen Angefordert: Schreiben Objekt Sicherheits­ deskriptor Verweigern Autoren Lesen, Schreiben Zulassen DaveC Lesen, Schreiben Verweigert Abbildung 7–9 Beispiel für die Zugriffsprüfung Es wäre nicht sehr wirtschaftlich, wenn das Sicherheitssystem die DACL jedes Mal verarbeiten würde, wenn ein Prozess ein Handle verwendet. Daher führt der SRM diese Zugriffsprüfung nur beim Öffnen des Handles durch, nicht bei jeder Verwendung. Sobald ein Prozess ein ­Handle erfolgreich geöffnet hat, kann das Sicherheitssystem die gewährten Zugriffsrechte daher nicht mehr widerrufen – nicht einmal, wenn sich die DACL des Objekts ändert. Da Kernelmoduscode für den Zugriff auf Objekte keine Handles verwendet, sondern Zeiger, erfolgt auch keine Zugriffsprüfung, wenn das Betriebssystem Objekte nutzt. In diesem Sinne vertraut die Windows-­ Exekutive also sich selbst und allen geladenen Treibern. Da der Besitzer eines Objekts immer den Zugriff vom Typ »DACL schreiben« auf ein Objekt hat, können Benutzer nicht daran gehindert werden, auf die Objekte zuzugreifen, die ihnen gehören. Hat ein Objekt aus irgendeinem Grunde eine leere DACL (kein Zugriff), kann der Besitzer das Objekt daher immer noch zum Schreiben der DACL öffnen und damit eine neue DACL mit den gewünschten Berechtigungen einrichten. Objekte schützen Kapitel 7 767
Vorsicht bei der Verwendung von grafischen Sicherheitseditoren! Wenn Sie die grafischen Berechtigungseditoren verwenden, um die Sicherheitseinstellungen für eine Datei, einen Registrierungsschlüssel, ein Active Directory-Objekt oder ein anderes sicherungsfähiges Objekt zu bearbeiten, wird im Hauptdialogfeld möglicherweise eine irreführende Ansicht angezeigt. Wenn Sie Vollzugriff für die Gruppe Jeder gewähren und für die Gruppe Administratoren verweigern, sieht es so aus, als würde der Zulassungseintrag für Jeder dem Verweigerungseintrag für Administratoren vorausgehen, weil sie in dieser Reihenfolge in der Oberfläche erscheinen. Wie wir aber bereits festgestellt haben, platzieren die Editoren Verweigerungseinträge vor Zulassungseinträgen. Erst die Registerkarte Berechtigungen des Dialogfelds Erweiterte Sicherheitseinstellungen zeigt die tatsächliche Reihenfolge der Zugriffssteuerungseinträge in der DACL. Doch selbst dieses Dialogfeld kann irreführend wirken, da bei komplizierteren DACLs erst die Verweigerungseinträge für verschiedene Arten des Zugriffs stehen können, auf die dann Zulassungseinträge für andere Arten des Zugriffs folgen. → 768 Kapitel 7 Sicherheit
Abgesehen von Ausprobieren besteht die einzige Möglichkeit, um herauszufinden, welchen Zugriff ein gegebener Benutzer oder eine Gruppe nun genau auf das Objekt hat, darin, einen Blick auf die Registerkarte Effektive Berechtigungen des Dialogfelds zu werfen, das nach einem Klick auf die Schaltfläche Erweitert im Eigenschaftendialogfeld angezeigt wird. Wenn Sie dort den Namen eines Benutzers oder einer Gruppe eingeben, können Sie anschließend ablesen, welche Berechtigungen diesem Benutzer oder der Gruppe für das betreffende Objekt gewährt werden. → Objekte schützen Kapitel 7 769
Dynamische Zugriffssteuerung Der in den vorherigen Abschnitten vorgestellte Mechanismus der besitzerverwalteten Zugriffssteuerung wurde schon in der ersten Version von Windows NT eingeführt und ist in vielen Situationen nützlich. Es gibt jedoch auch Fälle, für die er nicht flexibel genug ist. Beispielsweise ist es möglich, dass Benutzer von einem Computer an ihrem Arbeitsplatz auf eine freigegebene Datei zugreifen können müssen, von einem Computer zu Hause aber nicht darauf zugreifen dürfen. Mithilfe von Zugriffssteuerungseinträgen lässt sich eine solche Bedingung nicht angeben. In Windows 8 und Windows Server 2012 wurde die dynamische Zugriffssteuerung (Dynamic Access Control, DAC) eingeführt. Dieser flexible Mechanismus macht es möglich, Regeln der Grundlage benutzerdefinierter Attribute in Active Directory aufzustellen. DAC ersetzt den bisherigen Mechanismus nicht, sondern ergänzt ihn. Um eine Operation zu erlauben, muss die entsprechende Berechtigung also sowohl durch die dynamische Zugriffssteuerung als auch durch die klassische DACL gewährt werden. Abb. 7–10 zeigt die Hauptkomponenten der dynamischen Zugriffssteuerung. 770 Kapitel 7 Sicherheit
Benutzer­ansprüche Ressourcen­ eigenschaften Geräteansprüche Zentrale Zugriffsrichtlinie Abbildung 7–10 Bestandteile der dynamischen Zugriffssteuerung Ein Anspruch (Claim) ist eine Information über einen Benutzer, ein Gerät (Computer in einer Domäne) oder eine Ressource (allgemeines Attribut), die von einem Domänencontroller veröffentlicht wurde. Gültige Ansprüche sind beispielsweise der Titel eines Benutzers oder die Abteilungsklassifizierung einer Datei. In Ausdrücken zum Aufstellen von Regeln können Ansprüche beliebig kombiniert werden. Die Gesamtheit dieser Regeln bestimmt die zentrale Zugriffsrichtlinie. Die dynamische Zugriffssteuerung wird in Active Directory konfiguriert und durch Richtlinien durchgesetzt. Das Ticketprotokoll Kerberos wurde erweitert, um einen authentifizierten Transport von Benutzer- und Geräteansprüchen (das sogenannte Kerberos-Armoring) zu unterstützen. Die AuthZ-API Die Windows-API AuthZ bietet Autorisierungsfunktionen und implementiert dasselbe Sicherheitsmodell wie der Security Reference Monitor (SRM), was allerdings vollständig im Benutzermodus geschieht, und zwar in der Bibliothek %SystemRoot%\System32\Authz.dll. Dadurch bekommen Anwendungen, die ihre eigenen privaten Objekte schützen wollen – beispielsweise Datenbanktabellen –, die Möglichkeit, das Windows-Sicherheitsmodell zu nutzen, ohne sich dadurch die Kosten für die Übergänge vom Benutzer- in den Kernelmodus einzuhandeln, die bei der Nutzung von SRM auf sie zukämen. Die AuthZ-API verwendet standardmäßige Sicherheitsdeskriptoren, SIDs und Rechte. Zur Repräsentation der Clients wird anstelle von Tokens jedoch AUTHZ_CLIENT_CONTEXT genutzt. ­AuthZ enthält Benutzermodusvarianten aller Zugriffsprüfungs- und Windows-Sicherheitsfunktionen. Beispielsweise ist AuthzAccessCheck die AuthZ-Version der Windows-API AccessCheck, die die SRM-Funktion SeAccessCheck verwendet. Ein weiterer Vorteil besteht darin, dass Anwendungen AuthZ anweisen können, die Ergebnisse der Sicherheitsprüfungen zwischenzuspeichern, um kommende Prüfungen für denselben Clientkontext und Sicherheitsdeskriptor zu vereinfachen. AuthZ ist im Windows SDK vollständig dokumentiert. Diese Art der Zugriffsprüfung auf der Grundlage der SID und der Mitgliedschaft in einer Sicherheitsgruppe in einer statischen, kontrollierten Umgebung wird als identitätsgestützte Zugriffssteuerung (Identity-Based Access Control, IBAC) bezeichnet. Dazu muss das Sicherheitssys- Die AuthZ-API Kapitel 7 771
tem aber bereits die Identitäten aller möglichen Entitäten kennen, die auf ein Objekt zugreifen können, wenn die DACL in den Sicherheitsdeskriptor des Objekts gestellt wird. Windows unterstützt auch eine anspruchsgestützte Zugriffssteuerung (Claims-Based Access Control, CBAC). Dabei wird der Zugriff nicht auf der Grundlage der Identität oder Gruppenmitgliedschaft der zugreifenden Entität gewährt, sondern auf der Grundlage willkürlicher Attribute, die ihr zugewiesen und in ihrem Zugriffstoken gespeichert werden. Diese Attribute werden von einem Attributanbieter bereitgestellt, beispielsweise AppLocker. Der CBAC-Mechanismus bietet eine Reihe von Vorteilen. Unter anderem ist es damit möglich, eine DACL für einen Benutzer zu erstellen, dessen Identität noch nicht bekannt ist oder dynamisch mithilfe von Attributen bestimmt wird. CBAC-Zugriffssteuerungseinträge (auch bedingte Zugriffssteuerungseinträge genannt) werden in *-callback-Eintragsstrukturen gespeichert, die im Grunde genommen private Strukturen von AuthZ sind und von der System-API SeAccessCheck ignoriert werden. Die Kernelmodusroutine SeSrpAccessCheck kann bedingte Zugriffssteuerungseinträge nicht verstehen. Nur Anwendungen, die die AuthZ-APIs aufrufen, können CBAC nutzen. App­ Locker ist die einzige Systemkomponente, die CBAC verwendet, und legt damit Attribute wie den Pfad oder den Herausgeber fest. Drittanbieteranwendungen können CBAC mithilfe der CBAC-APIs von AuthZ nutzen. Die Verwendung von CBAC-Sicherheitsprüfungen macht vielseitige Verwaltungsrichtlinien möglich, beispielsweise die folgenden: QQ Es dürfen nur Anwendungen ausgeführt werden, die von der IT-Abteilung des Unternehmens genehmigt wurden. QQ Nur genehmigte Anwendungen dürfen auf Kontakte oder den Kalender in Microsoft Outlook zugreifen. QQ Nur Personen auf einer bestimmten Etage eines bestimmten Gebäudes dürfen auf die Drucker dieser Etage zugreifen. QQ Nur Vollzeitangestellte dürfen auf eine Intranetwebsite zugreifen. Die Attribute können in bedingten Zugriffssteuerungseinträgen verwendet werden, in denen ihr Vorhandensein, ihr Fehlen oder ihr Wert geprüft wird. Attributnamen können beliebige alphanumerische Unicode-Zeichen sowie Doppelpunkte, reguläre Schrägstriche und Unterstriche enthalten. Als Werte von Attributen sind 64-Bit-Integer, Unicode-Strings, Bytestrings und Arrays möglich. Bedingte Zugriffssteuerungseinträge Das Format von SDDL-Strings wurde erweitert, um auch Zugriffssteuerungseinträge mit bedingten Ausdrücken zu ermöglichen. Das neue Format sieht wie folgt aus: AceType;­ AceFlags;​ Rights;ObjectGuid;InheritObjectGuid;AccountSid;(ConditionalExpression). Der ACE-Typ eines bedingten Zugriffssteuerungseintrags ist entweder XA (für SDDL_­ CALLBACK_ACCESS_ALLOWED) oder XD (für SDDL_CALLBACK_ACCESS_DENIED). Zugriffssteuerungseinträge mit bedingten Ausdrücken werden nur für die anspruchsgestützte Autorisierung (insbesondere die AuthZ-APIs und AppLocker) verwendet und nicht vom Objekt-Manager und von Dateisystemen erkannt. 772 Kapitel 7 Sicherheit
In bedingten Ausdrücken können die Elemente aus Tabelle 7–9 verwendet werden. Element Beschreibung AttributeName Prüft, ob das angegebene Attribut einen von null verschiedenen Wert hat. exists AttributeName Prüft, ob das angegebene Attribut im Clientkontext vorhanden ist. AttributeName Operator Value Gibt das Ergebnis der angegebenen Operation zurück. In bedingten Ausdrücken können die Operatoren Contains any_of, ==, !=, <, <=, > und >= verwendet werden, um die Werte von Attributen zu prüfen. Es handelt sich dabei ausschließlich um binäre Operatoren (also keine unären). ConditionalExpression || ­ConditionalExpression Prüft, ob mindestens einer der bedingten Ausdrücke wahr ist. ConditionalExpression && ConditionalExpression Prüft, ob beide bedingten Ausdrücke wahr sind. !(ConditionalExpression) Negiert den bedingten Ausdruck. Member_of {SidArray} Prüft, ob das Array SID_AND_ATTRIBUTES des Clientkontexts alle Sicherheitskennungen (SIDs) enthält, die in der als SidArray angegebenen kommagetrennten Liste vorhanden sind. Tabelle 7–9 Mögliche Elemente für bedingte Ausdrücke Ein bedingter Zugriffssteuerungseintrag kann eine beliebige Menge von Bedingungen enthalten. Wenn die Bedingung zu false ausgewertet wird, wird der Eintrag ignoriert; lautet das Ergebnis dagegen true, wird er angewendet. Ein bedingter Eintrag kann mit der API ­Add­ConditionalAce zu einem Objekt hinzugefügt und mit AuthzAccessCheck überprüft werden. Mithilfe von bedingten Zugriffssteuerungseinträgen können Sie angeben, dass der Zugriff auf einzelne Datensätze in einem Programm nur einem Benutzer gewährt werden soll, der bestimmte Bedingungen erfüllt. Das können Bedingungen wie die folgenden sein: QQ Der Benutzer hat das Attribut Role mit dem Wert Architect, ProgramManager oder Development Lead und das Attribut Division mit dem Wert Windows. QQ Der Benutzer hat das Attribut ManagementChain mit dem Wert John Smith. QQ Der Benutzer hat das Attribut CommissionType mit dem Wert Officer und das Attribut PayGrade mit einem Wert größer als 6 (was im US-Militär einem Generalsrang entspricht). Windows enthält keine Programme, mit denen sich bedingte Zugriffssteuerungseinträge einsehen oder bearbeiten lassen. Privilegien und Kontorechte Bei vielen Operationen, die von Prozessen durchgeführt werden, kann die Autorisierung nicht über den Objektzugriffsschutz erfolgen, da dabei überhaupt keine Interaktion mit einem bestimmten Objekt stattfindet. Beispielsweise ist die Möglichkeit, die Sicherheitsprüfungen beim Öffnen von Dateien für die Datensicherung zu umgeben, ein Attribut eines Kontos und nicht Privilegien und Kontorechte Kapitel 7 773
eines Objekts. Mithilfe von Privilegien und Kontorechten können Systemadministratoren in Windows steuern, welche Konten sicherheitsrelevante Vorgänge ausführen dürfen. Ein Privileg (privilege; in der deutschen Version von Windows als »Recht« bezeichnet, hier zur besseren Unterscheidung jedoch »Privileg« genannt) erlaubt einem Konto, eine bestimmte Operation am System durchzuführen, z. B. den Computer herunterzufahren oder die Systemzeit zu ändern. Ein Kontorecht (account right) gewährt oder verweigert dem zugehörigen Konto die Möglichkeit, eine bestimmte Art von Anmeldung durchzuführen, z. B. eine lokale oder interaktive Anmeldung. Mithilfe des MMC-Snap-Ins Active Directory-Benutzer und Gruppen für Domänenkonto oder dem Editor für lokale Sicherheitsrichtlinien (%SystemRoot%\System32\secpol.msc) können Systemadministratoren Gruppen und Konten Privilegien und Kontorechte zuweisen. Abb. 7–11 zeigt den Abschnitt Zuweisen von Benutzerrechten im Editor für lokale Sicherheitsrichtlinien, in dem sämtliche auf Windows verfügbaren Privilegien und Kontorechte aufgeführt sind. In diesem Dienstprogramm wird übrigens nicht zwischen Privilegien und Kontorechten unterschieden. Die Kontorechte sind diejenigen, bei denen es um die Anmeldung geht. 774 Kapitel 7 Sicherheit
Abbildung 7–11 Zuweisung von Benutzerrechten im Editor für lokale Sicherheitsrichtlinien Kontorechte Kontorechte werden weder vom SRM durchgesetzt noch in Tokens gespeichert. Die für die Anmeldung zuständige Funktion ist LsaLogonUser. Wenn sich beispielsweise ein Benutzer interaktiv an einem Computer anmeldet, ruft Winlogon die API LogonUser auf und LogonUser wiederum LsaLogonUser. Dabei nimmt LogonUser einen Parameter entgegen, der die Art der Anmeldung angibt, z. B. eine interaktive Anmeldung, eine Netzwerk-, Batch- oder Dienstanmeldung oder eine Anmeldung über einen Terminalserver-Client. Privilegien und Kontorechte Kapitel 7 775
Als Reaktion auf Anmeldeanforderungen ruft die lokale Sicherheitsautorität (LSA) die dem Benutzer zugewiesenen Kontorechte von der LSA-Richtliniendatenbank ab. Sie vergleicht den Anmeldetyp mit dem Kontorecht des Benutzers und verweigert die Anmeldung, wenn das Konto nicht über das Recht verfügt, das diese Art der Anmeldung erlaubt, oder wenn ihm diese Art der Anmeldung verweigert wird. Tabelle 7–10 führt die in Windows definierten Kontorechte auf. Recht Bedeutung • Lokal anmelden verweigern • Lokal anmelden zulassen Für die interaktive Anmeldung direkt auf dem lokalen Computer • Zugriff vom Netzwerk auf diesen Computer verweigern • Auf diesen Computer vom Netzwerk aus zugreifen Für die Anmeldung von einem Remote­ computer aus • Anmelden über Remotedesktopdienste verweigern • Anmelden über Remotedesktopdienste zulassen Für die Anmeldung über einen Terminalserver-­ Client • Anmelden als Dienst verweigern • Anmelden als Dienst zulassen Wird vom Dienststeuerungs-Manager verwendet, wenn er einen Dienst in einem bestimmten Benutzerkonto startet • Anmelden als Batchauftrag verweigern • Anmelden als Stapelverarbeitungsauftrag Für Anmeldungen vom Typ »Batch« Tabelle 7–10 Kontorechte Mit den Funktionen LsaAddAccountRights und LsaRemoveAccountRights können Windows-Anmeldungen Kontorechte zu einem Konto hinzufügen und davon entfernen. Welche Kontorechte einem Konto zugewiesen sind, können sie mit LsaEnumerateAccountRights ermitteln. Privilegien Im Laufe der Zeit wurden immer mehr Privilegien in Windows definiert. Im Gegensatz zu den Kontorechten, die die LSA an einer zentralen Stelle durchsetzt, werden die einzelnen Privilegien von verschiedenen Komponenten definiert und auch von diesen durchgesetzt. Beispielsweise ist es der Prozess-Manager, der überprüft, ob das Debugrecht vorhanden ist, mit dem ein Prozess Sicherheitsprüfungen umgehen kann, wenn er mit OpenProcess das Handle eines anderen Prozesses öffnet. Tabelle 7–11 führt sämtliche Privilegien auf und gibt dabei an, wie und wann sie von den Systemkomponenten überprüft werden. Zu jedem Privileg ist in den SDK-Headern ein Makro in Form einer Rechtekonstanten definiert. Die Namen dieser Konstanten sind nach dem Muster SE_Privileg_NAME aufgebaut, z. B. SE_DEBUG_NAME für das Debugrecht. Die Werte der Konstanten sind Strings, die mit Se beginnen und mit Privilege enden, z. B. SeDebugPrivilege. Das scheint zu bedeuten, dass die Privilegien anhand von Strings bezeichnet werden, doch tatsächlich geschieht dies mithilfe von LUIDs, die bis zum Neustart eindeutig sind. Bei jedem Zugriff auf ein Privileg muss über die Funktion LookupPrivilegeValue der korrekte LUID nachgeschlagen werden. Allerdings können Ntdll- und Kernelcode Privilegien auch direkt mit Integerkonstanten bezeichnen, ohne einen LUID verwenden zu müssen. 776 Kapitel 7 Sicherheit
String Anzeigename Verwendung SeAssignPrimaryTokenPrivilege Ersetzen eines Tokens auf Prozessebene Wird von verschiedenen Komponenten geprüft, die das Token eines Prozesses festlegen, z. B. von NtSetInformationJobObject. SeAuditPrivilege Generieren von Sicherheitsüberwachungen Ist erforderlich, um mit Report­​ Event Ereignisse für das Sicherheitsprotokoll zu erzeugen. SeBackupPrivilege Sichern von Dateien und Verzeichnissen Veranlasst NTFS, unabhängig vom vorhandenen Sicherheitsdeskriptor den folgenden Zugriff auf beliebige Dateien oder Verzeichnisse zu geben: READ_CONTROL, ­ACCESS_SYSTEM_SECURITY, FILE_ GENERIC_READ und FILE_TRAVERSE. Beim Öffnen einer Datei für die Sicherung muss der Aufrufer das Flag FILE_FLAG_BACKUP_SEMANTICS angeben. Dieses Privileg gewährt auch den entsprechenden Zugriff auf Registrierungsschlüssel bei der Verwendung von RegSaveKey. SeChangeNotifyPrivilege Auslassen der durchsuchenden Überprüfung Wird von NTFS verwendet, um beim Nachschlagen in Verzeichnissen über mehrere Ebenen eine Prüfung der Berechtigungen für die Zwischenebenen auslassen zu können. Auch von Dateisystemen verwendet, wenn sich Anmeldungen für Benachrichtigungen über Änderungen an dem Dateisystem registrieren. SeCreateGlobalPrivilege Erstellen globaler Objekte Ist für Prozesse erforderlich, um Abschnittsobjekte und Objekte für symbolische Links in Verzeichnissen des Objekt-Manager-Namensraums zu erstellen, die anderen Sitzungen als der des Aufrufers zugewiesen sind. SeCreatePagefilePrivilege Erstellen einer Auslagerungsdatei Wird von der Funktion NtCreatePagingFile geprüft, die eine neue Auslagerungsdatei erstellt. SeCreatePermanentPrivilege Erstellen von dauerhaft freigegebenen Objekten Wird vom Objekt-Manager beim Erstellen eines dauerhaften Objekts geprüft (eines Objekts, dessen Zuweisung nicht aufgehoben wird, wenn keine Verweise mehr darauf bestehen). SeCreateSymbolicLinkPrivilege Erstellen symbolischer Verknüpfungen Wird von NTFS geprüft, wenn es mit CreateSymbolicLink symbolische Links im Dateisystem erstellt. SeCreateTokenPrivilege Erstellen eines Token­ objekts Wird von der Funktion NtCreateToken geprüft, die Tokenobjekte erstellt. → Privilegien und Kontorechte Kapitel 7 777
String Anzeigename Verwendung SeDebugPrivilege Debuggen von Programmen Wenn der Aufrufer dieses Privileg hat, erlaubt ihm der Prozess-Manager, mit NtOpenProcess und NtOpenThread auf beliebige Prozesse und Threads unabhängig von deren Sicherheitsdeskriptor zuzugreifen (mit Ausnahme von geschützten Prozessen). SeEnableDelegationPrivilege Ermöglichen, dass Computer- und Benutzerkonten für Delegierungszwecke vertraut wird Wird von Active Directory-Diensten für die Delegierung authentifizierter Konten verwendet. SeImpersonatePrivilege Annehmen der Clientidentität nach Authentifizierung Wird vom Prozess-Manager geprüft, wenn ein Thread für einen Identitätswechsel ein Token verwenden möchte, das für einen anderen Benutzer steht als Tokens des Prozesses, zu dem der Thread gehört. SeIncreaseBasePriorityPrivilege Anheben der Zeitplanungspriorität Wird vom Prozess-Manager geprüft und ist erforderlich, um die Priorität eines Prozesses anzuheben. SeIncreaseQuotaPrivilege Anpassen von Speicherkontingenten für einen Prozess Wird benötigt, um die Schwellenwerte für den Arbeitssatz, die Kontingente für den ausgelagerten und nicht ausgelagerten Pool und die CPU-Rate eines Prozesses zu ändern. SeIncreaseWorkingSetPrivilege Arbeitssatz eines Prozesses vergrößern Ist erforderlich, um SetProcessWorkingSetSize zum Anheben der Mindestgröße eines Arbeitssatzes aufzurufen. Dieses Privileg erlaubt dem Prozess auch indirekt, den Mindestarbeitssatz mit VirtualLock zu verriegeln. SeLoadDriverPrivilege Laden und Entfernen von Gerätetreibern Wird von den Treiberfunktionen NtLoadDriver und NtUnloadDriver geprüft. SeLockMemoryPrivilege Sperren von Seiten im Speicher Wird von NtLockVirtualMemory geprüft, der Kernelimplementierung von VirtualLock. SeMachineAccountPrivilege Hinzufügen von Arbeitsstationen zur Domäne Wird vom SAM auf einem Domänencontroller geprüft, wenn er ein Computerkonto in der Domäne anlegt. SeManageVolumePrivilege Durchführen von Volumewartungsaufgaben Wird von Dateisystemtreibern beim Öffnen eines Volumes zur Überprüfung der Festplatte und zur Defragmentierung benötigt. 778 Kapitel 7 Sicherheit →
String Anzeigename Verwendung SeProfileSingleProcessPrivilege Erstellen eines Profils für einen Einzelprozess Wird von SuperFetch und dem Prefetcher beim Abrufen von Informationen über einen einzelnen Prozess mithilfe von NtQuerySystemInformation geprüft. SeRelabelPrivilege Verändern einer Objekt­ bezeichnung Wird vom SRM geprüft, wenn er die Integritätsebene eines Objekts anhebt, das einem anderen Benutzer gehört, oder wenn er versucht, die Integritätsebene eines Objekts auf einen höheren Wert als den im Token des Aufrufers anzugeben. SeRemoteShutdownPrivilege Erzwingen des Herunterfahrens von einem Remote­system aus Winlogon prüft, ob Remoteaufrufer der Funktion Initiate­ SystemShutdown dieses Privileg haben. SeRestorePrivilege Wiederherstellen von Dateien und Verzeichnissen Veranlasst NTFS, unabhängig vom vorhandenen Sicherheitsdeskriptor den folgenden Zugriff auf beliebige Dateien oder Verzeichnisse zu geben: WRITE_DAC, WRITE_OWNER, ACCESS_SYSTEM_SECURITY, FILE_ GENERIC_WRITE, FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY und DELETE. Beim Öffnen einer Datei zur Wiederherstellung muss der Aufrufer das Flag FILE_FLAG_ BACKUP_SEMANTICS angeben. Dieses Privileg gewährt auch den entsprechenden Zugriff auf Registrierungsschlüssel bei der Verwendung von RegSaveKey. SeSecurityPrivilege Verwalten von Überwachungs- und Sicherheitsprotokollen Ist erforderlich, um auf die SACL eines Sicherheitsdeskriptors zuzugreifen und um das Sicherheitsprotokoll zu lesen und zu leeren. SeShutdownPrivilege Herunterfahren des ­Systems Wird von NtShutdownSystem und NtRaiseHardError geprüft. Die zweite Funktion zeigt in der interaktiven Konsole ein Dialogfeld mit einem Hinweis auf einen Systemfehler an. SeSyncAgentPrivilege Synchronisieren von Verzeichnisdienstdaten Ist erforderlich, um die Synchronisierungsdienste für LDAP-Verzeichnisse zu nutzen. Der Inhaber dieses Privilegs darf alle Objekte und Eigenschaften in dem Verzeichnis ungeachtet von deren Schutz lesen. → Privilegien und Kontorechte Kapitel 7 779
String Anzeigename Verwendung SeSystemEnvironmentPrivilege Verändern der Firmware­ umgebungsvariablen Wird von NtSetSystemEnvironmentValue und NtQuerySystem­ EnvironmentValue benötigt, um Firmwareumgebungsvariablen mithilfe der Hardwareabstrak­ tionsschicht (HAL) zu ändern und zu lesen. SeSystemProfilePrivilege Erstellen eines Profils der Systemleistung Wird von der Funktion NtCreateProfile geprüft, die zum Erstellen eines Systemprofils da ist und z. B. von dem Dienstprogramm Kernprof genutzt wird. SeSystemtimePrivilege Ändern der Systemzeit Ist erforderlich, um die Uhrzeit und das Datum zu ändern. SeTakeOwnershipPrivilege Übernehmen des Besitzes von Dateien und Objekten Ist erforderlich, um den Besitz eines Objekts zu übernehmen, ohne über besitzerverwaltete Zugriffsrechte zu verfügen. SeTcbPrivilege Einsetzen als Teil des ­Betriebssystems Wird vom SRM geprüft, um die Sitzungs-ID in einem Token festzulegen; vom PnP-Manager, um Plug-&-Play-Ereignisse zu erstellen und zu verwalten; von BroadcastSystemMessageEx beim Aufruf mit BSM_ALLDESKTOPS; von LsaRegisterLogonProcess; und bei der Kennzeichnung einer Anwendung als VDM mit NtSetInformationProcess. SeTimeZonePrivilege Ändern der Zeitzone Ist erforderlich, um die Zeitzone zu ändern. SeTrustedCredManAccessPrivilege Auf Anmeldeinformations-Manager als vertrauenswürdigem Aufrufer zugreifen Wird vom Anmeldeinformations-­ Manager geprüft, um zu bestätigen, dass er einem Aufrufer mit Anmeldeinformationen vertrauen soll, die als Klartext abgefragt werden können. Dieses Privileg wird standardmäßig nur Winlogon übertragen. SeUndockPrivilege Entfernen des Computers von der Dockingstation Wird vom PnP-Manager im Benutzermodus geprüft, wenn das Abdocken des Computers ausgelöst oder das Auswerfen eines Geräts angefordert wird. SeUnsolicitedInputPrivilege Nicht angeforderte Daten von einem Terminalgerät empfangen Dieses Privileg wird in Windows zurzeit nicht verwendet. Tabelle 7–11 Privilegien Um zu prüfen, ob ein Token ein bestimmtes Privileg einschließt, können Komponenten im Benutzermodus PrivilegeCheck oder LsaEnumerateAccountRights und im Kernelmodus S­ eSinglePrivilegeCheck und SePrivilegeCheck verwenden. Die Privilege-APIs verstehen keine Kontorechte, die AccountRights-APIs aber auch Privilegien. 780 Kapitel 7 Sicherheit
Im Gegensatz zu Kontorechten können Privilegien aktiviert und deaktiviert werden. Für eine erfolgreiche Prüfung auf ein Privileg muss es nicht nur in dem angegebenen Token vorhanden, sondern auch aktiviert sein. Dadurch soll erreicht werden, dass Privilegien nur dann aktiviert werden, wenn sie erforderlich sind, damit ein Prozess nicht versehentlich eine privilegierte sicherheitsrelevante Operation ausführen kann. Die Aktivierung und Deaktivierung von Privilegien erfolgt mithilfe der Funktion AdjustTokenPrivileges. Experiment: Die Aktivierung eines Privilegs beobachten Mit der folgenden Vorgehensweise können Sie beobachten, wie das Systemsteuerungsapplet Datum und Uhrzeit (auf Windows 10) das Privileg SeTimeZonePrivilege aktiviert, wenn Sie die Schnittstelle verwenden, um die Zeitzone auf Ihrem Computer zu ändern: 1. Starten Sie Process Explorer mit erhöhten Rechten. 2. Rechtsklicken Sie auf die Uhr in der Taskleiste und wählen Sie Datum/Uhrzeit ändern. Alternativ können Sie auch die App Einstellungen starten und nach Zeit suchen, um die Seite mit den Datums- und Uhrzeiteinstellungen zu öffnen. 3. Rechtsklicken Sie in Process Explorer auf den Prozess SystemSettings.exe, wählen Sie Eigenschaften und klicken Sie im Eigenschaftendialogfeld auf die Registerkarte Security. Wie Sie sehen, ist das Privileg SeTimeZonePrivilege deaktiviert. → Privilegien und Kontorechte Kapitel 7 781
4. Ändern Sie die Zeitzone, schließen Sie das Eigenschaftendialogfeld und öffnen Sie es erneut. Jetzt können Sie auf der Registerkarte Security erkennen, dass SeTimeZone­Privilege aktiviert ist: Experiment: Das Privileg »Auslassen der durchsuchenden Überprüfung« Systemadministratoren müssen sich des Privilegs Auslassen der durchsuchenden Überprüfung (SeNotifyPrivilege) und dessen Konsequenzen bewusst sein. Das folgende Experiment zeigt, dass mangelnde Kenntnisse dieses Privilegs zu Sicherheitslücken führen können: 1. Erstellen Sie einen Ordner und darin eine neue Textdatei mit beliebigem Beispieltext. 2. Suchen Sie die Datei im Explorer auf, öffnen Sie ihr Eigenschaftendialogfeld und klicken Sie auf die Registerkarte Sicherheit. 3. Klicken Sie auf Erweitert. 4. Deaktivieren Sie das Kontrollkästchen Vererbung. 5. Wenn Sie gefragt werden, ob Sie geerbte Berechtigungen entfernen oder kopieren wollen, wählen Sie Kopieren. → 782 Kapitel 7 Sicherheit
6. Ändern Sie die Sicherheitseinstellungen des neuen Ordners so, dass Ihr Konto keinen Zugriff darauf hat. Wählen Sie dazu Ihr Konto aus und aktivieren Sie in der Liste der Berechtigungen alle Kontrollkästchen mit der Bezeichnung Verweigern. 7. Starten Sie den Editor. Wählen Sie Datei > Öffnen und versuchen Sie, in dem daraufhin angezeigten Dialogfeld zu dem neuen Verzeichnis zu gelangen. Der Zugriff auf dieses Verzeichnis wird Ihnen verweigert. 8. Geben Sie in das Feld Dateiname des Dialogfelds Öffnen den Namen der neuen Datei mit vollständiger Pfadangabe ein. Die Datei wird geöffnet. Wenn Ihr Konto das Privileg Auslassen der durchsuchenden Überprüfung nicht hat und Sie versuchen, eine Datei zu öffnen, führt NFTS für jedes Verzeichnis im Pfad zu der Datei eine Zugriffsprüfung durch. Dabei würde Ihnen in unserem Beispiel der Zugriff auf die Datei verweigert. Superprivilegien Einige Privilegien sind so umfassend, dass Benutzer, die darüber verfügen, zu »Superbenutzern« mit voller Kontrolle über den Computer werden. Diese Privilegien können auf unterschiedlichste Weise verwendet werden, um nicht autorisierten Zugriff zu erlangen oder um Beschränkungen für Ressourcen aufzuheben und nicht autorisierte Operationen durchzuführen. Wir wollen uns hier jedoch darauf konzentrieren, die Privilegien zur Ausführung von Code zu nutzen, der dem Benutzer weitere, ihm nicht zustehende Rechte gewähren kann, denn damit kann der Benutzer letzten Endes jede beliebige Operation auf dem Computer ausführen. In diesem Abschnitt geben wir einen Überblick über die »Superprivilegien« und erklären, wie sie ausgenutzt werden können. Andere Privilegien können ebenfalls missbraucht werden, z. B. Sperren von Seiten im Speicher (SeLockMemoryPrivilege) für Denial-of-Service-Angriffe auf ein System, aber damit wollen wir uns hier nicht befassen. Auf Systemen mit eingeschalteter Benutzerkontensteuerung werden die hier vorgestellten Privilegien nur Anwendungen auf mindestens hoher Integritätsebene zugewiesen, selbst wenn das Konto über sie verfügt. QQ Debuggen von Programmen (SeDebugPrivilege) Ein Benutzer mit diesem Privileg kann jegliche Prozesse im System (mit Ausnahme von geschützten Prozessen) unabhängig von deren Sicherheitsdeskriptoren öffnen. Beispielsweise könnte er ein Programm einsetzen, das den Lsass-Prozess öffnet, ausführbaren Code in dessen Adressraum schreibt und dann mit CreateRemoteThread einen Thread erstellt, der den injizierten Code in einem ­Sicherheitskontext mit höheren Rechten ausführt. Dieser Code wiederum kann dem Benutzer weitere Rechte und Gruppenmitgliedschaften gewähren. QQ Übernehmen des Besitzes (SeTakeOwnershipPrivilege) Dieses Privileg erlaubt dem Inhaber, den Besitz über beliebige sicherungsfähige Objekte zu übernehmen (selbst über geschützte Prozesse und Threads), indem er seine eigene SID in das Besitzerfeld des Sicherheitsdeskriptors für das Objekt schreibt. Da der Besitzer immer die Berechtigung zum Lesen und Ändern der DACL des Sicherheitsdeskriptors hat, kann ein Prozess mit diesem Privileg die DACL bearbeiten, um sich selbst Vollzugriff auf das Objekt zu geben, und es Privilegien und Kontorechte Kapitel 7 783
dann schließen und mit Vollzugriff öffnen. Dadurch kann der Benutzer nicht nur sensible Daten einsehen, sondern sogar Systemdateien wie Lsass, die im Rahmen des normalen Systembetriebs ausgeführt werden, durch seine eigenen Programme ersetzen, die einem Benutzer erhöhte Rechte gewähren. QQ Wiederherstellen von Dateien und Verzeichnissen (SeRestorePrivilege) Ein Benutzer mit diesem Privileg kann sämtliche Dateien auf dem System durch seine eigenen ersetzen, sogar Systemdateien. QQ Laden und Entfernen von Gerätetreibern (SeLoadDriverPrivilege) Böswillige Benutzer sind mit diesem Privileg in der Lage, Gerätetreiber in dem System zu laden. Da Gerätetreiber als vertrauenswürdige Bestandteile des Betriebssystems angesehen werden und mit den Anmeldeinformationen des Systemkontos ausgeführt werden können, kann ein solcher Treiber privilegierte Programme starten, die dem Benutzer noch weitere Rechte zuweisen. QQ Erstellen eines Tokenobjekts (SeCreateTokenPrivilege) Dieses Privileg lässt sich dazu missbrauchen, Tokens für willkürliche Benutzerkonten mit willkürlichen Gruppenmitgliedschaften und Privilegien zu erstellen. QQ Einsetzen als Teil des Betriebssystems (SeTcbPrivilege) Die Funktion LsaRegisterLogonProcess, die ein Prozess aufruft, um eine vertrauenswürdige Verbindung zu Lsass herzustellen, prüft, ob dieses Privileg vorhanden ist. Ein böswilliger Benutzer mit diesem Privileg kann eine vertrauenswürdige Lsass-Verbindung einrichten und dann die Funktion LsaLogonUser ausführen, um weitere Anmeldesitzungen zu erstellen. LsaLogonUser verlangt einen gültigen Benutzernamen und ein Kennwort und akzeptiert eine optionale Liste von SIDs, die sie dem ursprünglichen Token für eine neue Anmeldesitzung hinzufügt. Daher könnte der Benutzer mit seinem eigenen Benutzernamen und Kennwort eine neue Anmeldesitzung erstellen, bei der die SIDs von Gruppen und Benutzern mit höheren Rechten in das resultierende Token eingeschlossen werden. Die Verwendung erhöhter Rechte ist auf den lokalen Computer beschränkt und wirkt sich nicht auf das Netzwerk aus, da jegliche Interaktion mit einem anderen Rechner eine Authentifizierung auf einem Domänencontroller und die Überprüfung des Domänenkennworts erfordert. Da Domänenkennwörter aber weder im Klartext noch in verschlüsselter Form auf einem lokalen Computer gespeichert sind, sind sie für Schadcode nicht zugänglich. Zugriffstokens von Prozessen und Threads Abb. 7–12 gibt einen Überblick über die bis jetzt in diesem Kapitel eingeführten Begriffe und zeigt die grundlegenden Sicherheitsstrukturen von Prozessen und Threads. Wie Sie sehen, verfügen die Prozess-, Thread- und Zugriffstokenobjekte alle über ihre eigenen Zugriffssteu­ erungslisten (Access Control Lists, ACLs). In der Abbildung haben Thread 2 und 3 jeweils ein Identitätswechseltoken, während Thread 1 das Prozesszugriffstoken verwendet. 784 Kapitel 7 Sicherheit
Zugriffstoken Prozess Benutzer-SID Gruppen-SIDs Rechte Besitzer-SID Standard-ACL … Zugriffstoken Benutzer-SID Gruppen-SIDs Rechte Besitzer-SID Standard-ACL … Zugriffstoken Benutzer-SID Gruppen-SIDs Rechte Besitzer-SID Standard-ACL … Abbildung 7–12 Sicherheitsstrukturen von Prozessen und Threads Sicherheitsüberwachung Der Objekt-Manager kann Überwachungsereignisse als Resultat von Zugriffsprüfungen generieren. Windows-Funktionen, die für Benutzer zur Verfügung stehen, sind in der Lage, solche Ereignisse auch direkt zu erstellen. Kernelmoduscode hat stets die Erlaubnis, Überwachungsereignisse zu erzeugen. Mit der Überwachung sind die beiden Rechte SeSecurityPrivilege und SeAuditPrivilege verbunden. Um das Sicherheitsprotokoll zu verwalten und die SACL eines Objekts einzusehen und festzulegen, muss ein Prozess über das Recht SeSecurityPrivilege verfügen. Prozesse, die Systemdienste für die Überwachung aufrufen, müssen jedoch über das Recht SeAuditPrivilege verfügen, um einen Überwachungsdatensatz erstellen zu können. Die Überwachungsrichtlinie des lokalen Systems regelt, welche Arten von Sicherheitsereignissen überwacht werden. Sie gehört zu der lokalen Sicherheitsrichtlinie, die Lsass auf dem lokalen System unterhält. Konfiguriert wird sie mit dem Editor für lokale Sicherheitsrichtlinien aus Abb. 7–13. Ihre Einstellungen (sowohl die Grundeinstellungen unter Lokale Richtlinien als auch diejenigen unter Erweiterte Überwachungsrichtlinienkonfiguration) sind als Bitmapwert im Registrierungsschlüssel HKEY_LOCAL_MACHINE\SECURITY\Policy\PolAdtEv gespeichert. Sicherheitsüberwachung Kapitel 7 785
Abbildung 7–13 Die Überwachungsrichtlinie im Editor für lokale Sicherheitsrichtlinien Bei der Systeminitialisierung und bei Änderungen der Richtlinie sendet Lsass dem SRM Nachrichten, um ihn über die Überwachungsrichtlinie zu informieren. Der SRM wiederum sendet über seine ALPC-Verbindung Überwachungsdatensätze, die er auf der Grundlage der Überwachungsereignisse erstellt, an Lsass, und Lsass bearbeitet sie und sendet sie an die Ereignisprotokollierung. Diese Weiterleitung der Datensätze erfolgt durch Lsass und nicht durch den SRM, da Lsass noch wichtige Einzelheiten hinzufügt, z. B. die erforderlichen Angaben, um den überwachten Prozess vollständig identifizieren zu können. Die Ereignisprotokollierung schreibt die Überwachungsdatensätze anschließend in das Sicherheitsprotokoll. Neben den Überwachungsdatensätzen vom SRM erstellen auch Lsass und der SAM Überwachungsdatensätze, die Lsass direkt an die Ereignisprotokollierung sendet. Mithilfe der AuthZ-APIs können Anwendungen selbst definierte Überwachungen erzeugen. Abb. 7–14 zeigt den Gesamtablauf. 786 Kapitel 7 Sicherheit
Sicherheitsteilsystem Sicherheitsprotokoll LSA-Authentifizierung Ereignis­ protokollierung AuthZ-API LSA-Überwachung Benutzermodus Überwachungsrichtlinie Überwachungs­ datensätze Kernelmodus Objekt-Manager E/A-Analyse NTFS Mailslots Benannte Pipes Konfigurations-Manager Prozess-Manager Abbildung 7–14 Weiterleitung von Überwachungsdatensätzen Die Überwachungsdatensätze werden bei ihrem Eingang im SRM in eine Warteschlange zur Übertragung an die LSA gestellt. Sie werden nicht in Stapeln gesendet. Die Verschiebung vom SRM zum Sicherheitsteilsystem kann auf zwei verschiedene Weisen erfolgen. Kleine Überwachungsdatensätze (mit einem Umfang unter der maximalen Nachrichtengröße für ALPCs) werden als ALPC-Nachrichten gesendet. Dabei werden die Datensätze vom Adressraum des SRM in den Adressraum des Lsass-Prozesses kopiert. Größere Überwachungsdatensätze macht der SRM in gemeinsam genutztem Speicher für Lsass verfügbar und übergibt lediglich einen Zeiger darauf in einer ALPC-Nachricht. Überwachung des Objektzugriffs Eine wichtige Anwendung des Überwachungsmechanismus in vielen Umgebungen besteht darin, ein Protokoll der Zugriffsversuche auf abgesicherte Objekte zu führen, insbesondere auf Dateien. Dazu muss die Richtlinie Objektzugriffsversuche überwachen aktiviert sein. Außerdem muss es in den SACLs Überwachungseinträge geben, die die Überwachung der betreffenden Objekte erlauben. Wenn versucht wird, das Handle eines Objekts zu öffnen, ermittelt der SRM zunächst, ob dieser Vorgang zugelassen oder verweigert werden soll. Ist die Objektzugriffsüberwachung eingeschaltet, untersucht der SRM anschließend die SACL des Objekts. Es gibt zwei Arten von Überwachungseinträgen, nämlich Zulassungs- und Verweigerungseinträge. Damit ein Überwachungseintrag für den Objektzugriff erstellt wird, muss der Überwachungseintrag mit allen Sicherheitskennungen der zugreifenden Entität und mit allen angeforderten Zugriffsmethoden übereinstimmen und von dem Typ sein, der dem Ergebnis der Zugriffsprüfung entspricht (Zugriff erlaubt oder verweigert). In Überwachungsdatensätzen für den Objektzugriff wird nicht nur festgehalten, ob der Zugriff gewährt oder verweigert wurde, sondern auch der Grund für den Erfolg bzw. Fehlschlag. Das geschieht gewöhnlich in Form eines Zugriffssteuerungseintrags, der in SDDL (Security Descriptor Definition Language) angegeben ist. Wenn der Zugriff auf ein Objekt Sicherheitsüberwachung Kapitel 7 787
entgegen den Erwartungen verweigert oder zugelassen wird, ist dadurch eine Diagnose möglich, da der Zugriffssteuerungseintrag, der für den Erfolg oder Fehlschlag des Zugriffs führte, angegeben ist. Wie Sie in Abb. 7–13 gesehen haben, ist die Überwachung des Objektzugriffs (ebenso wie alle anderen Überwachungsrichtlinien) standardmäßig ausgeschaltet. Experiment: Den Objektzugriff überwachen Die Objektzugriffsüberwachung können Sie auf folgende Weise beobachten: 1. Begeben Sie sich im Explorer zu einer Datei, auf die Sie normalerweise Zugriff haben (z. B. eine Textdatei), öffnen Sie ihr Eigenschaftendialogfeld, rufen Sie die Registerkarte Sicherheit auf und klicken Sie auf Erweitert. 2. Rufen Sie die Registerkarte Überwachung auf und klicken Sie sich durch die Warnung über administrative Rechte. In dem schließlich angezeigten Dialogfeld können Sie zur SACL der Datei Überwachungseinträge hinzufügen. 3. Klicken Sie auf Hinzufügen und wählen Sie Prinzipal auswählen. → 788 Kapitel 7 Sicherheit
4. Geben Sie in dem anschließend eingeblendeten Dialogfeld Benutzer oder Gruppe auswählen Ihren Benutzernamen oder den Namen einer Gruppe ein, zu der Sie gehören, z. B. Jeder. Klicken Sie auf Namen überprüfen und dann auf OK. Dadurch erscheint das Dialogfeld, um einen Überwachungseintrag für den Benutzer bzw. die Gruppe und die Datei zu erstellen. 5. Klicken Sie dreimal auf OK, um das Eigenschaftendialogfeld zu schließen. 6. Doppelklicken Sie im Explorer auf die Datei, um sie mit dem vorgesehenen Programm zu öffnen (z. B. eine Textdatei mit dem Editor). 7. Öffnen Sie das Startmenü, geben Sie ereignis ein und klicken Sie auf Ereignisanzeige. 8. Öffnen Sie das Sicherheitsprotokoll. Für den Zugriff auf die Datei gibt es keinen Eintrag, da die Überwachungsrichtlinie für den Objektzugriff noch nicht eingerichtet wurde. 9. Öffnen Sie im Editor für lokale Sicherheitsrichtlinien Lokale Richtlinien > Überwachungsrichtlinie. 10. Doppelklicken Sie auf Objektzugriffsversuche überwachen und aktivieren Sie die Option Erfolgreich, um einen erfolgreichen Zugriff auf Dateien zu überwachen. 11. Klicken Sie im Menü der Ereignisanzeige auf Aktion > Aktualisieren. Die Änderungen an der Überwachungsrichtlinie führen dazu, dass Überwachungsdatensätze angezeigt werden. 12. Doppelklicken Sie im Explorer auf die Datei, um sie erneut zu öffnen. 13. Klicken Sie in der Ereignisanzeige abermals auf Aktion > Aktualisieren. Jetzt sind mehrere Überwachungsdatensätze über Dateizugriffe vorhanden. → Sicherheitsüberwachung Kapitel 7 789
14. Suchen Sie den Überwachungsdatensatz mit der Ereignis-ID 4656. Er wird mit der Beschreibung »Ein Handle zu einem Objekt wurde angefordert ...« angezeigt. Sie können auch die Suchfunktion nutzen, um nach dem Namen der geöffneten Datei zu suchen. 15. Scrollen Sie in dem Textfeld nach unten bis zum Abschnitt Zugriffsgründe. Im folgenden Beispiel sind dort die beiden Zugriffsmethoden READ_CONTROL und SYNCHRONIZE sowie ReadAttributes, ReadEA (erweiterte Attribute) und ReadDate angefordert worden. READ_CONTROL wurde zugelassen, da es der Besitzer der Datei war, der den Zugriff versucht hat. Die anderen Zugriffsarten wurden aufgrund der Einstellungen im Zugriffssteuerungseintrag gewährt. Globale Überwachungsrichtlinie Neben Zugriffssteuerungseinträgen für einzelne Objekte können Sie auch eine globale Überwachungsrichtlinie aufstellen, um die Objektzugriffsüberwachung für alle Objekte im Dateisystem, für alle Registrierungsschlüssel oder beides einzurichten. Damit können Sie dafür sorgen, dass die gewünschte Überwachung stattfindet, ohne sich die SACLs aller einzelnen Objekte ansehen zu müssen. Administratoren können die globale Überwachungsrichtlinie mit dem Befehl AuditPol und der Option /resourceSACL festlegen und einsehen. Innerhalb eines Programms lässt sich dies mit einem Aufruf von AuditSetGlobalSacl bzw. AuditQueryGlobalSacl bewerkstelligen. Ebenso wie für Änderungen an Objekt-SACLs ist auch für Änderungen an diesen globalen SACLs das Recht SeSecurityPrivilege erforderlich. 790 Kapitel 7 Sicherheit
Experiment: Die globale Überwachungsrichtlinie einrichten Mit dem Befehl AuditPol können Sie die globale Überwachungsrichtlinie einschalten. 1. Öffnen Sie den Editor für lokale Sicherheitsrichtlinien, öffnen Sie die Einstellungen für die Überwachungsrichtlinie (siehe Abb. 7–13), doppelklicken Sie auf Objektzugriffsversuche überwachen und aktivieren Sie sowohl Erfolgreich als auch Fehler. Auf den meisten Systemen gibt es nicht viele SACLs, die eine Objektzugriffsüberwachung angeben, weshalb an dieser Stelle wenn überhaupt nur sehr wenige Überwachungsdatensätze erzeugt werden. 2. Geben Sie an einer Eingabeaufforderung mit erhöhten Rechten den folgenden Befehl ein. Dadurch wird ein Überblick der Befehle zum Festlegen und Abrufen der globalen Überwachungsrichtlinie angezeigt. C:\> auditpol /resourceSACL 3. Geben Sie nun an derselben Eingabeaufforderung mit erhöhten Rechten die folgenden Befehle ein. Gewöhnlich wird dabei gemeldet, dass für die entsprechenden Ressourcentypen keine globale SACL vorhanden ist. (Beachten Sie, dass es bei den Schlüsselwörtern File und Key auf die Groß- und Kleinschreibung ankommt!) C:\> auditpol /resourceSACL /type:File /view C:\> auditpol /resourceSACL /type:Key /view 4. Geben Sie an derselben Eingabeaufforderung mit erhöhten Rechten den folgenden Befehl ein. Dadurch richten Sie eine globale Überwachungsrichtlinie ein, gemäß der jegliche Versuche des angegebenen Benutzers, Dateien für den Schreibzugriff (FW) zu öffnen, Überwachungsdatensätze hervorrufen, und zwar unabhängig davon, ob diese Versuche erfolgreich waren oder nicht. Bei dem Benutzernamen kann es sich um einen einzelnen Benutzernamen auf dem System, einen Gruppennamen wie Jeder, einen Benutzernamen mit Domänenangabe (Domänenname\Benutzername) oder eine SID handeln. C:\> auditpol /resourceSACL /set /type:File /user:Ihr_Benutzername /success /failure /access:FW 5. Öffnen Sie unter dem angegebenen Benutzernamen im Explorer oder mithilfe anderer Programme eine Datei. Suchen Sie anschließend im Systemprotokoll nach den Überwachungsdatensätzen. 6. Entfernen Sie nach Beendigung des Experiments die globale SACL aus Schritt 4 wieder wie folgt mit dem Befehl auditpol: C:\> auditpol /resourceSACL /remove /type:File /user:Ihr_Benutzername Die globale Überwachungsrichtlinie wird in Form von zwei SACLs in den Registrierungsschlüsseln HKLM\SECURITY\Policy\GlobalSaclNameFile und HKLM\SECURITY\Policy\GlobalSaclNameKey gespeichert. Sie können sich diese Schlüssel ansehen, indem Sie Regedit.exe unter dem System- Sicherheitsüberwachung Kapitel 7 791
konto ausführen, wie es im Abschnitt »Systemkomponenten für die Sicherheit« weiter vorn in diesem Kapitel beschrieben wird. Diese Schlüssel werden erst dann angelegt, wenn die zugehörigen globalen SACLs zum ersten Mal eingerichtet werden. Objektspezifische SACLs können die globale Überwachungsrichtlinie nicht überschreiben, aber ergänzen. Wenn beispielsweise die globale Überwachungsrichtlinie die Überwachung des Lesezugriffs aller Benutzer auf alle Dateien verlangt, können SACLs einzelner Dateien eine zusätzliche Überwachung des Schreibzugriffs auf diese Dateien durch bestimmte Benutzer oder Gruppen fordern. Die globale Überwachungsrichtlinie kann auch im Editor für lokale Sicherheitsrichtlinien unter Erweiterte Überwachungsrichtlinienkonfiguration eingerichtet werden. Damit beschäftigen wir uns im nächsten Abschnitt. Erweiterte Überwachungsrichtlinienkonfiguration Neben den bereits beschriebenen Einstellungen für die Überwachungsrichtlinie bietet der Editor für lokale Sicherheitsrichtlinien unter Erweiterte Überwachungsrichtlinienkonfiguration auch detailliertere Steuerungsmöglichkeiten (siehe Abb. 7–15). Abbildung 7–15 Die Erweiterte Überwachungsrichtlinienkonfiguration im Editor für lokale Sicherheitsrichtlinien Zu jeder der neun Überwachungsrichtlinieneinstellungen unter Lokale Richtlinien (siehe Abb. 7–13) gibt es hier eine Gruppe von Einstellungen, die eine feinere Steuerung ermöglichen. So können Sie mit den Einstellungen für die Überwachung des Objektzugriffs unter Lokale 792 Kapitel 7 Sicherheit
Richtlinien nur dafür sorgen, dass der Zugriff auf sämtliche Objekte überwacht wird, während die Einstellungen in diesem Abschnitt es ermöglichen, den Zugriff auf verschiedene Arten von Objekten getrennt voneinander zu überwachen. Wenn Sie eine der Einstellungen unter Lokale Richtlinien aktivieren, werden dadurch implizit alle zugehörigen erweiterten Einstellungen eingeschaltet. Brauchen Sie eine genauere Kontrolle über den Inhalt des Überwachungsprotokolls, können Sie die erweiterten Einstellungen einzeln festlegen. Die Standardeinstellungen werden dann zu einem Produkt der erweiterten Einstellungen, was im Editor für lokale Sicherheitsrichtlinien aber nicht erkennbar ist. Ein Versuch, die Überwachung sowohl mit den einfachen als auch den erweiterten Einstellungen festzulegen, kann zu unerwarteten Ergebnissen führen. Mit der erweiterten Option Globale Objektzugriffsüberwachung können Sie die im vorherigen Abschnitt beschriebenen globalen SACLs mit einer ähnlichen grafischen Oberfläche einrichten, wie sie im Explorer und im Registrierungseditor für Sicherheitsdeskriptoren im Dateisystem bzw. der Registrierung verwendet wird. Anwendungscontainer In Windows 8 wurde die neue Sicherheits-Sandbox AppContainer eingeführt. Anwendungscontainer sind zwar hauptsächlich für UWP-Prozesse gedacht, können aber auch für »normale« Prozesse verwendet werden (wobei es jedoch kein mitgeliefertes Tool für diesen Zweck gibt). In diesem Abschnitt beschäftigen wir uns hauptsächlich mit Paket-Anwendungscontainern, also denen für UWP-Prozesse mit dem .appx-Format. Eine vollständige Erörterung von UWP-Apps würde den Rahmen dieses Kapitels sprengen. Weitere Informationen dazu finden Sie in Kapitel 3 sowie in den Kapiteln 8 und 9 von Band 2. Hier konzentrieren wir uns auf die Sicherheits­ aspekte von Anwendungscontainern und ihre typische Verwendung als Hosts für UWP-Anwendungen. UWP-App (Universal Windows Platform) ist der neueste Begriff zur Bezeichnung von Prozessen, die die Windows-Laufzeit beherbergen. Früher wurden sie auch Vollbild-Apps, moderne Apps, Metro-Apps und manchmal auch einfach WindowsApps genannt. Der Namensbestandteil »Universal« gibt an, dass diese Anwendungen zur Bereitstellung und Ausführung auf verschiedenen Windows 10-Editionen und Gerätearten von IoT Core über Mobil- und Desktopgeräte und die Xbox bis hin zur HoloLens gedacht sind. Im Grunde genommen sind sie jedoch noch mit denen identisch, die in Windows 8 eingeführt wurden. Das Prinzip der Anwendungscontainer in diesem Abschnitt gilt dafür für Windows 8 und höher. Beachten Sie, dass statt UWP manchmal auch die Bezeichnung UAP (für Universal Application Platform) verwendet wird. Beide Begriffe bedeuten jedoch dasselbe. Der ursprüngliche Codename für AppContainer war LowBox. Dieser Begriff taucht noch in vielen API-Namen und Datenstrukturen auf, die wir uns in diesem Abschnitt ansehen. Anwendungscontainer Kapitel 7 793
UWP-Apps im Überblick Die Mobilgeräterevolution hat zu neuen Möglichkeiten geführt, um Software zu erwerben und auszuführen. Mobilgeräte beziehen ihre Anwendungen gewöhnlich von einem zentralen »Store«, wobei Installation und Aktualisierung automatisch erfolgen und nur sehr wenig Benutzereingriff erfordern. Sobald ein Benutzer im Store eine App ausgewählt hat, sieht er die Berechtigungen, die für das Funktionieren der Anwendung erforderlich sind. Diese Berechtigungen werden als Fähigkeiten (capabilities) bezeichnet und beim Einreichen in den Store zusammen mit dem Paket angegeben. Dadurch kann der Benutzer entscheiden, ob die geforderten Fähigkeiten für ihn akzeptabel sind. Abb. 7–16 zeigt die Fähigkeitenliste eines UWP-Spiels (die Beta-Edition von Minecraft für Windows 10): Das Spiel benötigt Internetzugriff als Client und als Server sowie Zugriff auf das lokale Heim- oder Arbeitsplatznetz. Mit dem Herunterladen des Spiels akzeptiert der Benutzer stillschweigend, dass das Spiel diese Fähigkeiten nutzen kann. Auf der anderen Seite kann der Benutzer aber darauf vertrauen, dass das Spiel nur diese Fähigkeiten nutzt. Das heißt, es kann keine zusätzlichen, nicht genehmigten Fähigkeiten anwenden, indem es etwa auf die Kamera des Geräts zugreift. Abbildung 7–16 Teil der Seite für eine App im Store mit Angabe der Fähigkeiten Um ein Gefühl für den Unterschied zwischen UWP-Apps und (klassischen) Desktopanwendungen zu bekommen, schauen Sie sich Tabelle 7–12 an. Wie sich die Windows-Plattform für Entwickler darstellt, sehen Sie in Abb. 7–17. 794 Kapitel 7 Sicherheit
UWP-App (Klassische) Desktopanwendung Geräteunterstützung Läuft auf allen Windows-Geräten. Nur auf PCs APIs Kann auf WinRT-, einen Teil der COM- und einen Teil der Win32-APIs zugreifen. Kann auf COM-, Win32- und einen Teil der WinRT-APIs zugreifen. Identität Starke Anwendungsidentität (statisch und dynamisch) Rohe EXE-Dateien und Prozesse Information Deklaratives APPX-Manifest Undurchsichtige Binärdateien Installation Eigenständiges APPX-Paket Lose Dateien oder MSI Anwendungsdaten Isolierter Speicher für einzelne Benutzer/Anwendungen (lokal und Roaming) Freigegebene Benutzerprofile Lebenszyklus Teilnahme an Anwendungsressourcenverwaltung und PLM Lebenszyklus auf Prozessebene Instanzen Nur eine einzige Instanz Beliebige Anzahl von Instanzen Tabelle 7–12 UWP- und Desktopanwendungen im Vergleich (Klassische) Desktopanwendungen .NET-Sprachen .NET-Laufzeit UWP-Apps Desktop Bridge („Centennial“) Web Bridge („Westminster“) iOS Bridge („Islandwood“) .NETSprachen Universal Windows Platform (Windows-Laufzeit-APIs) DLLs des Win32-Teilsystems Benutzermodus Kernelmodus Exekutive und Kernel Abbildung 7–17 Aufbau der Windows-Plattform Sehen wir uns einige der Elemente in Abb. 7–17 noch etwas genauer an: QQ UWP-Apps können ebenso wie Desktopanwendungen normale ausführbare Dateien produzieren. Als Host für UWP-App auf der Grundlage von HTML oder JavaScript wird Wwahost.exe (%SystemRoot%\System32\wwahost.exe) verwendet, da diese Apps eine DLL produzieren und keine ausführbare Datei. QQ Die UWP wird durch Windows-Laufzeit-APIs implementiert, die wiederum auf einer erweiterten Version von COM basieren. Sprachprojektionen werden für C++ (durch proprietäre, als C++/CX bezeichnete Spracherweiterungen), .NET-Sprachen und JavaScript bereitgestellt. Diese Projektionen machen es Entwicklern relativ einfach, von ihrer vertrauten Umgebung aus auf die Typen, Methoden, Eigenschaften und Ereignisse von WinRT zuzugreifen. Anwendungscontainer Kapitel 7 795
QQ Es stehen verschiedene Bridge-Technologien zur Verfügung, um auch andere Arten von Anwendungen in UWP zu übersetzen. Weitere Informationen zur Nutzung dieser Technologien finden Sie in der MSDN-Dokumentation. QQ Die Windows-Laufzeit befindet sich ebenso wie das .NET-Framework oberhalb der DLLs des Windows-Teilsystems. Sie hat keine Kernelkomponenten und gehört nicht zu einem anderen Teilsystem, da sie dieselben Win32-APIs nutzt, die das System anbietet. Einige Richtlinien und die allgemeine Unterstützung für Anwendungscontainer sind jedoch im Kernel implementiert. QQ Die Windows-Laufzeit-APIs sind in DLLs implementiert, die sich im Verzeichnis %SystemRoot%\System32 befinden und Namen der Form Windows.Xxx.Yyy...dll tragen, wobei der Dateiname gewöhnlich den von der API implementierten Namensraum angibt. So stellt beispielsweise Windows.Globalization.dll die Klassen bereit, die sich im Namensraum Windows.Globalization befinden. (Eine vollständige Referenz der WinRT-APIs finden Sie in der MSDN-Dokumentation.) Anwendungscontainer Die erforderlichen Schritte, um Prozesse zu erstellen, haben Sie bereits in Kapitel 3 kennengelernt. Außerdem haben Sie auch einige der zusätzlichen Schritte gesehen, die nötig sind, um UWP-Prozesse anzulegen. Bei Letzteren wird der Erstellvorgang vom Dienst DCOMLaunch ausgelöst, da UWP-Pakete unter anderem das Launch-Protokoll unterstützen. Der resultierende Prozess wird in einem Anwendungscontainer ausgeführt. Paketprozesse in einem Anwendungscontainer weisen unter anderem die folgenden Eigenschaften auf: QQ Die Integritätsebene des Prozesses ist auf Niedrig gesetzt, was automatisch den Zugriff auf viele Objekte sowie auf bestimmte APIs für den Prozess beschränkt, wie Sie in diesem Kapitel schon gesehen haben. QQ UWP-Prozesse werden immer innerhalb eines Jobs erstellt (ein Job pro UWP-App). Dieser Job verwaltet den UWP-Prozess sowie alle Hintergrundprozesse, die für ihn ausgeführt werden (mithilfe verschachtelter Jobs). Dadurch kann der Prozessstatus-Manager (PSM) die App oder die Hintergrundverarbeitung auf einen Streich anhalten oder wieder aufnehmen. QQ Das Token für einen UWP-Prozess verfügt über eine Anwendungscontainer-SID, die auf dem SHA-2-Hash des UWP-Paketnamens basiert. Wie Sie noch sehen werden, wird diese SID vom System und von anderen Anwendungen verwendet, um den Zugriff auf Dateien und andere Kernelobjekte ausdrücklich zu erlauben. Die SID gehört nicht zur NT-­AUTORITÄT, die Sie in diesem Kapitel bis jetzt vor allem gesehen haben, sondern zur ZERTIFIZIERUNGSSTELLE FÜR ANWENDUNGSPAKETE (die in der deutschen Version von Windows leider so genannt wurde, obwohl es sich dabei nicht um eine Zertifizierungsstelle handelt). Im Stringformat beginnt diese SID daher mit S-1-15-2, was SECURITY_AP_PACKAGE_BASE_RID (15) und SECURITY_APP_ PACKAGE_BASE_RID (2) entspricht. Da ein SHA-2-Hash 32 Bytes umfasst, umfasst der Rest der SID insgesamt acht RIDs (wie bereits erwähnt, handelt es sich bei einem RID um einen 4-Byte-ULONG-Wert). 796 Kapitel 7 Sicherheit
QQ Das Token kann einen Satz von Fähigkeiten einschließen, die jeweils durch eine SID dargestellt werden. Diese Fähigkeiten werden im Manifest der Anwendung deklariert und auf der Seite der Anwendung im Store angezeigt. Bei der Speicherung im Fähigkeitsabschnitt des Manifests werden sie anhand der in Kürze beschriebenen Regeln ins SID-Format umgewandelt. Sie gehören zu der SID-Autorität aus dem vorherigen Punkt, haben aber den Standard-RID SECURITY_CAPABILITY_BASE_RID (3). Komponenten der Windows-Laufzeit, Gerätezugriffsklassen im Benutzermodus und der Kernel können sich die Fähigkeiten ansehen, um einzelne Operationen zuzulassen oder zu verweigern. QQ Das Token kann nur die Rechte SeChangeNotifyPrivilege, SeIncreaseWorkingSetPrivilege, SeShutdownPrivilege, SeTimeZonePrivilege und SeUndockPrivilege enthalten. Dies ist der Standardsatz von Rechten, die mit Standardbenutzerkonten verknüpft werden. Auf bestimmten Geräten kann darüber hinaus die Funktion AppContainerPrivilegesEnabledExt der API-Erweiterung ms-win-ntos-ksecurity vorhanden sein, um die standardmäßig aktivierten Privilegien noch weiter einzuschränken. QQ Das Token enthält bis zu vier Sicherheitsattribute (siehe den Abschnitt zur attributgestützten Zugriffssteuerung weiter vorn in diesem Kapitel), die angeben, dass das Token zu der UWP-Paketanwendung gehört. Diese Attribute werden wie bereits angedeutet vom Dienst DcomLaunch hinzugefügt, der für Aktivierung von UWP-Anwendungen zuständig ist. Es handelt sich dabei um folgende Attribute: • WIN://PKG Dieses Attribut gibt an, dass das Token zu einer UWP-Paketanwendung gehört. Es enthält einen Integerwert mit dem Ursprung der Anwendung sowie einige Flags. Die möglichen Werte sind in Tabelle 7–13 und 7–14 aufgeführt. • WIN://SYSAPPID Enthält die Anwendungskennungen (oder Paket- oder String­ namen) in Form eines Arrays aus Unicode-Stringwerten. • WIN://PKGHOSTID Gibt bei Paketen mit einem ausdrücklich festgelegten UWP-­ Pakethost dessen ID in Form eines Integerwerts an. • WIN://BGKD   Dieses Attribut wird nur für Hintergrundhosts verwendet (z. B. Back­ groundTaskHost.exe für allgemeine Hintergrundtasks), die als COM-Anbieter ausgeführ­ te UWP-Paketdienste speichern können. Der Name dieses Attributs soll back­ground bedeuten. Es enthält einen Integerwert für die Host-ID. Das Flag TOKEN_LOWBOX (0x4000) wird im Element Flags des Tokens gesetzt, das mithilfe verschiedener Windows- und Kernel-APIs abgefragt werden kann (z. B. mit GetTokenInformation). Das ermöglicht Komponenten, beim Vorhandensein eines Anwendungscontainertokens auf andere Weise zu identifizieren und sich zu verhalten. Es gibt noch eine zweite Art von Anwendungscontainer, nämlich Kindanwendungscontainer. Sie werden verwendet, wenn ein UWP-Anwendungscontainer (oder Eltern-Anwendungscontainer) eigene verschachtelte Anwendungscontainer erstellt. Neben den acht RIDs, die mit denen seines Elterncontainers übereinstimmen, verfügt ein Kindanwendungscontainer zur eindeutigen Identifizierung noch über vier weitere RIDs. Anwendungscontainer Kapitel 7 797
Ursprung Bedeutung Unknown (0) Der Ursprung des Pakets ist unbekannt. Unsigned (1) Das Paket ist nicht signiert. Inbox (2) Das Paket gehört zu einer integrierten (im Lieferumfang enthaltenen) Windows-Anwendung. Store (3) Das Paket gehört zu einer aus dem Store heruntergeladenen UWP-Anwendung. Um den Ursprung zu bestätigen, wird geprüft, ob die DACL der ausführbaren Hauptdatei der Anwendung einen Vertrauenseintrag enthält. Developer Unsigned (4) Das Paket gehört zu einem nicht signierten Entwicklerschlüssel. Developer Signed (5) Das Paket gehört zu einem signierten Entwicklerschlüssel. Line-of-Business (6) Das Paket gehört zu einer quergeladenen Branchenanwendung. Tabelle 7–13 Paketursprünge Flag Bedeutung PSM_ACTIVATION_TOKEN_PACKAGED_­ APPLICATION (0x1) Gibt an, dass die UWP-Anwendung im AppX-Paketformat gespeichert ist. Dies ist die Standardeinstellung. PSM_ACTIVATION_TOKEN_SHARED_ENTITY (0x2) Gibt an, dass das Token für mehrere ausführbare Dateien verwendet wird, die alle zur selben AppX-UWP-­ Paketanwendung gehören. PSM_ACTIVATION_TOKEN_FULL_TRUST (0x4) Gibt an, dass das Anwendungscontainertoken für das Hosting einer Win32-Anwendung verwendet wird, die mit der Windows-Bridge Centennial für Desktopanwendungen konvertiert wurde. PSM_ACTIVATION_TOKEN_NATIVE_SERVICE (0x8) Gibt an, dass das Anwendungscontainertoken für das Hosting eines Paketdienstes verwendet wird, der vom Ressourcen-Manager des Dienststeuerungs-Managers erstellt wurde. Mehr Informationen über Dienste erhalten Sie in Kapitel 9 von Band 2. PSM_ACTIVATION_TOKEN_DEVELOPMENT_APP (0x10) Gibt an, dass es sich um eine interne Entwicklungsanwendung handelt. Wird auf Endbenutzersystemen nicht verwendet. BREAKAWAY_INHIBITED (0x20) Das Paket kann keine Prozesse erstellen, die nicht selbst Pakete sind. Dieses Flag wird vom Prozesserstellungsattribut PROC_THREAD_ATTRIBUTE_DESKTOP_APP_ POLICY festgelegt (siehe Kapitel 3). Tabelle 7–14 Paketflags 798 Kapitel 7 Sicherheit
Experiment: Informationen über UWP-Prozesse einsehen Es gibt mehrere Möglichkeiten, um sich UWP-Prozesse anzusehen. Process Explorer kann Prozesse, die die Windows-Laufzeit nutzen, farbig hervorheben (in der Grundeinstellung hellblau). Um das auszuprobieren, starten Sie Process Explorer, öffnen das Menü Options und wählen Configure Colors. Aktivieren Sie das Kontrollkästchen Immersive Processes. »Immersive processes« (in der deutschen Lokalisierung »Vollbildprozesse« genannt) war der ursprüngliche Begriff zur Beschreibung von WinRT-Apps (den heutigen UWP-Apps) in Windows 8. (Der Begriff rührt daher, dass es sich dabei meistens um Vollbildanwendungen handelte.) Die Unterscheidung zwischen Vollbild- und anderen Prozessen erfolgt durch Aufruf der API IsImmersiveProcess. Starten Sie Calc.exe und schalten Sie wieder zu Process Explorer um. Jetzt werden mehrere Prozesse hellblau hervorgehoben, darunter Calculator.exe. Wenn Sie die Rechner-Anwendung minimieren, wird die hellblaue Hervorhebung grau, weil der Taschenrechner angehalten wurde. Stellen Sie das Taschenrechnerfenster wieder her, und die Hervorhebung wird wieder hellblau angezeigt. Das Gleiche können Sie auch bei anderen Apps feststellen, etwa bei Cortana (SearchUI. exe). Klicken oder tippen Sie auf das Cortana-Symbol in der Taskleiste und schließen Sie das Fenster. Dabei können Sie beobachten, wie die graue Hervorhebung hellblau wird und danach wieder grau. Wenn Sie auf die Startschaltfläche klicken oder tippen, wird ShellExperienceHost.exe ähnlich hervorgehoben. → Anwendungscontainer Kapitel 7 799
Bei einigen Prozessen mag die hellblaue Hervorhebung überraschend wirken, etwa bei Explorer.exe, TaskMgr.exe und RuntimeBroker.exe. Es handelt sich dabei nicht um echte Apps, aber da sie die Windows-Laufzeit-APIs nutzen, werden sie ebenfalls als Vollbildprozesse eingestuft. (Was es mit RuntimeBroker auf sich hat, werden wir uns in Kürze ansehen.) Blenden Sie in Process Explorer die Spalte Integrity ein und sortieren Sie die Anzeige nach dieser Spalte. Dabei werden Sie feststellen, dass Prozesse wie Calculator.exe und ­SearchUI.exe die Integritätsebene AppContainer aufweisen. Explorer und TaskMgr befinden sich jedoch nicht auf dieser Ebene, was deutlich zeigt, dass es sich bei ihnen nicht um UWP-Prozesse handelt, sondern dass sie ihren eigenen Regeln folgen. 800 Kapitel 7 Sicherheit
Experiment: Ein Anwendungscontainertoken einsehen Die Eigenschaften eines Prozesses in einem Anwendungscontainer können Sie sich mit verschiedenen Tools ansehen. Auf der Registerkarte Security von Process Explorer werden die in dem Token vermerkten Fähigkeiten angezeigt. Für Calculator.exe sieht diese Registerkarte wie folgt aus: Beachten Sie vor allem die Anwendungscontainer-SID, die als AppContainer in der Spalte Flags angezeigt wird, und eine einzelne Fähigkeit unmittelbar darunter. Die Basis-RIDs (­SECURITY_APP_PACKAGE_BASE_RID bzw. SECURITY_CAPABILITY_BASE_RID) unterscheiden sich, aber die restlichen acht RIDs sind identisch. Beide SIDs enthalten wie bereits erklärt den Paketnamen im SHA-2-Format. Dies zeigt, dass es immer eine implizite Fähigkeit gibt, nämlich die Fähigkeit, das Paket selbst zu sein. Die Taschenrechneranwendung braucht also in Wirklichkeit gar keine Fähigkeit. Im Abschnitt über Fähigkeiten weiter hinten lernen Sie ein viel anspruchsvolleres Beispiel kennen. Anwendungscontainer Kapitel 7 801
Experiment: Attribute von Anwendungscontainertokens einsehen Ähnliche Informationen können Sie mit dem Sysinternals-Tool AccessChk auch an der Befehlszeile gewinnen. Dabei werden außerdem noch sämtliche Attribute des Tokens angezeigt. Wenn Sie beispielsweise AccessChk mit den Schaltern -p -f und der Prozess-ID für SearchUI.exe (Cortana) ausführen, ergibt sich Folgendes: C:\ >accesschk -p -f 3728 Accesschk v6.10 - Reports effective permissions for securable objects Copyright (C) 2006-2016 Mark Russinovich Sysinternals - www.sysinternals.com [7416] SearchUI.exe RW DESKTOP-DD6KTPM\aione RW NT AUTHORITY\SYSTEM RW Package \S-1-15-2-1861897761-1695161497-2927542615-642690995-327840285-26597451352630312742 Token security: RW DESKTOP-DD6KTPM\aione RW NT AUTHORITY\SYSTEM RW DESKTOP-DD6KTPM\aione-S-1-5-5-0-459087 RW Package \S-1-15-2-1861897761-1695161497-2927542615-642690995-327840285-26597451352630312742 R BUILTIN\Administrators Token contents: User: DESKTOP-DD6KTPM\aione AppContainer: Package \S-1-15-2-1861897761-1695161497-2927542615-642690995-327840285-26597451352630312742 Groups: Mandatory Label\Low Mandatory Level INTEGRITY Everyone MANDATORY NT AUTHORITY\Local account and member of Administrators group DENY ... Security Attributes: WIN://PKGHOSTID TOKEN_SECURITY_ATTRIBUTE_TYPE_UINT64 → 802 Kapitel 7 Sicherheit
[0] 1794402976530433 WIN://SYSAPPID TOKEN_SECURITY_ATTRIBUTE_TYPE_STRING [0] Microsoft.Windows.Cortana_1.8.3.14986_neutral_neutral_ cw5n1h2txyewy [1] CortanaUI [2] Microsoft.Windows.Cortana_cw5n1h2txyewy WIN://PKG TOKEN_SECURITY_ATTRIBUTE_TYPE_UINT64 [0] 131073 TSA://ProcUnique [TOKEN_SECURITY_ATTRIBUTE_NON_INHERITABLE] [TOKEN_SECURITY_ATTRIBUTE_COMPARE_IGNORE] TOKEN_SECURITY_ATTRIBUTE_TYPE_UINT64 [0] 204 [1] 24566825 An erster Stelle steht die Pakethost-ID im Hexadezimalformat: 0x6600000000001. Da alle Pakethost-IDs mit 0x66 beginnen, bedeutet dies, dass Cortana die erste verfügbare Host-ID verwendet, nämlich 1. Darauf folgen die Systemanwendungs-IDs, die drei Strings enthalten: den starken Paketnamen, den Anzeigenamen für die Anwendung und den vereinfachten Paketnamen. Schließlich gibt es noch den Paketanspruch mit dem Hexwert 0x20001. Die steht für den Ursprung Inbox (2) und die Flags PSM_ACTIVATION_TOKEN_PACKAGED_APPLICATION, was bedeutet, dass es sich bei Cortana um einen Bestandteil eines AppX-Pakets handelt. Sicherheitsumgebung von Anwendungscontainern Eine der größten Nebenwirkungen aufgrund des Vorhandenseins einer Anwendungscon­ tainer-SID und der zugehörigen Flags besteht darin, dass der Algorithmus aus dem Abschnitt »Zugriffsprüfungen« weiter vorn in diesem Kapitel dahin gehend verändert wird, dass er alle regulären Benutzer- und Gruppen-SIDs in dem Token ignoriert, also praktisch als reine Verweigerungseinträge behandelt. Wenn also beispielsweise der Taschenrechner von Max Mustermann gestartet wird, der zu den Gruppen Benutzer und Jeder gehört, so schlagen trotzdem alle Zugriffsprüfungen fehl, die den SIDs von Max Mustermann und den Gruppen Benutzer und Jeder Zugriff gewähren. Die einzigen SIDs, die sich der Algorithmus zur besitzerverwalteten Zugriffsprüfung ansieht, sind die Anwendungscontainer-SIDs, gefolgt von den Fähigkeits-SIDs, die anschließend der Algorithmus für die Fähigkeitsprüfung untersucht. Anwendungscontainertokens aber sorgen nicht nur dafür, dass SIDs für die besitzerverwaltete Zugriffssteuerung als reine Verweigerungs-SIDs behandelt werden, sondern ändern den Zugriffsprüfungsalgorithmus noch auf eine weitere wichtige Weise: Eine NULL-DACL, bei deren Vorhandensein gewöhnlich Vollzugriff für alle gestattet wird, weil keine anderweitigen Informationen vorliegen (dies ist etwas anderes als eine leere DACL, bei der der Zugriff aufgrund ausdrücklicher Zulassungsregeln für alle verweigert wird!), wird ignoriert und als Verweigerung Anwendungscontainer Kapitel 7 803
angesehen. Einfacher wird die Sache dadurch, dass ein Anwendungscontainer nur auf solche sicherungsfähigen Objekte zugreifen kann, die über einen Zulassungseintrag für seine Anwendungscontainer-SID oder eine seiner Fähigkeiten verfügt. Sogar nicht gesicherte Objekte (also solche mit NULL-DACLs) sind aus dem Spiel. Bis jetzt haben wir stillschweigend vorausgesetzt, dass jeder UWP-Paketanwendung ein Anwendungscontainertoken entspricht. Das heißt jedoch nicht unbedingt, dass mit einem Anwendungscontainer nur eine einzige ausführbare Datei verknüpft sein kann. UWP-Pakete können mehre ausführbare Dateien enthalten, die alle zum selben Anwendungscontainer gehören, z. B. für das Back-End und das Front-End eines Mikrodienstes. Dadurch können sie sich alle dieselbe SID und dieselben Fähigkeiten teilen und Daten untereinander austauschen. Das führt jedoch zu Kompatibilitätsproblemen. Wie kann eine Anwendung ohne Zugriff auf selbst die grundlegendsten Dateisystem-, Registrierungs- und Objekt-Manager-Ressourcen funktionieren? Windows berücksichtigt dies und stellt jeweils eine eigene Ausführungsumgebung (ein »Gefängnis«, wenn Sie so wollen) für jeden Anwendungscontainer auf: QQ Im Namensraum des Objekt-Managers wird unter \Sessions\x\AppContainerNamedObjects ein privates Unterverzeichnis für benannte Kernelobjekte angelegt und erhält die Stringdarstellung der Anwendungscontainer-SID als Name. Das Objekt dieses Unterverzeichnisses erhält eine Zugriffssteuerungsliste mit einer Zugriffsmaske, die der SID des Anwendungscontainers Vollzugriff erlaubt. Das steht im Gegensatz zu Desktopanwendungen, die (innerhalb der Sitzung x) alle das Unterverzeichnis \Sessions\x\BaseNamedObjects verwenden. Die Konsequenzen dieser Vorgehensweise und das Erfordernis, dass im Token jetzt auch Handles gespeichert werden müssen, sehen wir uns in Kürze genauer an. QQ Das Token enthält eine LowBox-Nummer. Dabei handelt es sich um einen eindeutigen Bezeichner für die Elemente eines Arrays, das der Kernel in der globalen Variablen g_SessionLowBoxArray speichert. Jedes dieser Elemente verweist auf eine SEP_LOWBOX_NUMBER_ENTRY-Struktur. Der wichtigste Inhalt dieser Strukturen ist jeweils eine Atomtabelle ausschließlich für den Anwendungscontainer, da der Kernelmodustreiber des Windows-Teilsystems (Win32k.sys) den Anwendungscontainern keinen Zugriff auf die globale Atomtabelle gewährt. QQ Das Dateisystem unterhält in %LOCALAPPDATA% das Verzeichnis Packages, das wiederum Unterverzeichnisse mit den Paketnamen (die Stringversionen der Anwendungscon­tainerSIDs) aller installierten UWP-Anwendungen enthält. Jedes dieser Anwendungsverzeichnisse wiederum enthält anwendungsspezifische Verzeichnisse wie TempState, RoamingState, Settings, LocalCache usw. Sie sind jeweils mit einer Zugriffssteuerungsliste versehen, deren Zugriffsmaske der Anwendungscontainer-SID Vollzugriff erlaubt. QQ Im Verzeichnis Settings gibt es die Datei Settings.dat. Dabei handelt es sich um eine Datei für einen Registrierungshive, die als Anwendungshive geladen wird. (Mehr über Anwendungshives erfahren Sie in Kapitel 9 von Band 2.) Der Hive fungiert als lokale Registrierung für die Anwendung, in der WinRT-APIs die verschiedenen persistenten Status der Anwendung speichern. Auch die Zugriffssteuerungslisten dieser Registrierungseinträge gewähren der SID des zugehörigen Anwendungscontainers wieder Vollzugriff. 804 Kapitel 7 Sicherheit
Mithilfe dieser vier »Gefängnisse« können Anwendungscontainer ihr Dateisystem, ihre Regis­ trierung und ihre Atomtabelle auf sichere Weise lokal speichern, ohne dazu Zugriff auf sensible Benutzer- und Systembereiche zu benötigen. Was aber ist mit einem möglichen Zugriff – zumindest Lesezugriff – auf kritische Systemdateien (wie Ntdll.dll und Kernel32.dll), Registrierungsschlüssel (diejenigen, die diese Bibliotheken benötigen) oder benannte Objekte (etwa den ALPC-Port \RPC Control\DNSResolver für DNS-Lookups)? Es wäre nicht sehr sinnvoll, bei jeder Installation oder Deinstallation einer UWP-Anwendung die Zugriffssteuerungslisten für ganze Verzeichnisse, Registrierungsschlüssel und Objektnamensräume zu ändern, um mehrere SIDs hinzuzufügen bzw. zu entfernen. Zur Lösung dieses Problem gibt es die besondere Gruppen-SID ALL APPLICATION PACKAGES, die automatisch mit jedem Anwendungscontainertoken verknüpft wird. Viele kritische Systemspeicherorte wie %SystemRoot%\System32 und HKLM\Software\Microsoft\Windows\CurrentVersion enthalten diese SID in ihren DACLs, wobei die Zugriffsmaske gewöhnlich den Zugriff zum Lesen oder zum Lesen und Ausführen erlaubt. Auch bestimmte Objekte im Namensraum des Objekt-Managers verfügen über diese SID, z. B. der ALPC-Port DNSResolver im Objekt-Manager-Verzeichnis \RPC Control. Das Gleiche gilt auch für manche COM-Objekte, die das Ausführungsrecht gewähren. Auch wenn es nicht offiziell dokumentiert ist, können Drittanbieterentwickler beim Erstellen von Nicht-UWP-Anwendungen Interaktionen mit UWP-Anwendungen erlauben, indem sie diese SID für ihre eigenen Ressourcen verwenden. Da UWP-Anwendungen im Rahmen ihrer WinRT-Bedürfnisse rein technisch in der Lage sind, fast jede Win32-DLL zu laden (da WinRT auf Win32 aufbaut), und es schwer vorherzusagen ist, was eine einzelne UWP-Anwendung alles benötigt, ist mit den DACLs vieler Systemressourcen leider die SID ALL APPLICATION PACKAGES vorausschauend verknüpft. Dadurch ist es für UWP-Entwickler unmöglich, zu verhindern, dass von ihrer Anwendung aus beispielsweise DNS-Lookups erfolgen. Dieser mehr als erforderliche Zugriff hilft auch den Autoren von Exploits, die ihn ausnutzen können, um aus der Sandbox des Anwendungscontainers auszubrechen. Um dieses Risiko einzudämmen, gibt es in neuen Versionen von Windows 10 (beginnend mit Version 1607, also dem Anniversary Update) die neue Sicherheitsvorkehrung der eingeschränkten Anwendungscontainer. Wird das Prozessattribut PROC_THREAD_ATTRIBUTE_ALL_APPLICATION_PACKAGES_POLICY beim Erstellen des Prozesses auf PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT gesetzt (mehr über Prozessattribute erfahren Sie in Kapitel 3), so wird das Token mit keinerlei Zugriffssteuerungseinträgen verknüpft, in denen die SID ALL APPLICATION PACKAGES angegeben ist. Dadurch wird der Zugriff auf viele Systemressourcen abgeschnitten, die sonst zugänglich wären. Solche Tokens lassen sich an dem Vorhandensein eines vierten Tokenattributs namens WIN://­ NOALLAPPPKG mit einem Integerwert von 1 erkennen. Das bringt uns jedoch zu unserem alten Problem zurück: Wie kann eine solche Anwendung auch nur Ntdll.dll laden, die für die Prozessinitialisierung von entscheidender Bedeutung ist? In Windows 10 Version 1607 wurde die neue Gruppe ALL RESTRICTED APPLICATION PACKAGES eingeführt, um dieses Problem zu lösen. Unter anderem enthält das Verzeichnis System32 jetzt diese SID mit Lese- und Ausführungsberechtigungen, da das Laden von DLLs in diesem Verzeichnis für viele Prozesse in Sandboxes unverzichtbar ist. Der ALPC-Port DNSResolver jedoch hat diese SID nicht, weshalb ein solcher Anwendungscontainer den Zugriff auf DNS verlieren kann. Anwendungscontainer Kapitel 7 805
Experiment: Die Sicherheitsattribute eines Anwendungscontainers einsehen In diesem Experiment sehen wir uns die Sicherheitsattribute einiger der im vorherigen Abschnitt beschriebenen Verzeichnisse an. 1. Starten Sie die Taschenrechneranwendung. 2. Starten Sie das Sysinternals-Tool WinObj mit erhöhten Rechten und begeben Sie sich zu dem Objektverzeichnis, das nach der Anwendungscontainer-SID des Taschenrechners benannt ist. (Dieses Verzeichnis haben Sie bereits in einem früheren Experiment gesehen.) → 806 Kapitel 7 Sicherheit
3. Rechtsklicken Sie auf das Verzeichnis, wählen Sie Properties und rufen Sie die Registerkarte Security auf. Wie Sie im folgenden Screenshot sehen, hat die Anwendungscon­ tainer-SID des Taschenrechners die Berechtigungen zum Auflisten und zum Hinzufügen von Objekten und Unterverzeichnissen (sowie weitere, die über einen Bildlauf zu erreichen sind). Das bedeutet, dass die Taschenrechneranwendung in diesem Verzeichnis Kernelobjekte erstellen kann. 4. Öffnen Sie den lokalen Ordner des Taschenrechners unter %LOCALAPPDATA%\ Packages\Microsoft.WindowsCalculator_8wekyb3d8bbwe. Rechtsklicken Sie auf das Unterverzeichnis Settings, wählen Sie Properties und klicken Sie auf die Registerkarte Security. Dabei können Sie erkennen, dass die Anwendungscontainer-SID des Rechners Vollzugriff auf diesen Ordner hat: → Anwendungscontainer Kapitel 7 807
5. Öffnen Sie im Explorer das Verzeichnis %SystemRoot% (z. B. C:\Windows), rechtsklicken Sie auf das Verzeichnis System32, wählen Sie Properties und klicken Sie auf die Registerkarte Security. Hier können Sie erkennen, dass allen Anwendungspaketen und allen eingeschränkten Anwendungspaketen (bei Windows 10 Version 1607 und höher) Leseund Ausführungsberechtigungen eingeräumt wird. Die gleichen Informationen können Sie auch mit dem Befehlszeilenwerkzeug AccessChk von Sysinternals einsehen. Experiment: Die Atomtabelle des Anwendungscontainers einsehen Bei einer Atomtabelle handelt es sich um eine Hashtabelle, in der über Integerwerte zugängliche Strings gespeichert sind. Sie wird von Fenstersystemen in verschiedenen Zusammenhängen zur Identifizierung verwendet, z. B. bei der Fensterklassenregistrierung (RegisterClassEx) und bei benutzerdefinierten Windows-Nachrichten. Die private Atomtabelle eines Anwendungscontainers können Sie sich mit dem Kerneldebugger ansehen: 1. Führen Sie die Taschenrechneranwendung aus, öffnen Sie WinDbg und starten Sie eine lokale Kerneldebuggersitzung. 2. Suchen Sie den Prozess des Taschenrechners: lkd> !process 0 1 calculator.exe PROCESS ffff828cc9ed1080 SessionId: 1 Cid: 4bd8 Peb: d040bbc000 ParentCid: 03a4 DeepFreeze DirBase: 5fccaa000 ObjectTable: ffff950ad9fa2800 HandleCount: <Data Not Accessible> → 808 Kapitel 7 Sicherheit
Image: Calculator.exe VadRoot ffff828cd2c9b6a0 Vads 168 Clone 0 Private 2938. Modified 3332. Locked 0. DeviceMap ffff950aad2cd2f0 Token ffff950adb313060 ... 3. Verwenden Sie den gefundenen Tokenwert in den folgenden Ausdrücken: lkd> r? @$t1 = @$t0->NumberOfBuckets lkd> r? @$t0 = (nt!_RTL_ATOM_TABLE*)((nt!_token*)0xffff950adb313060)>LowboxNumberEntry->AtomTable lkd> .for (r @$t3 = 0; @$t3 < @$t1; r @$t3 = @$t3 + 1) { ?? (wchar_t*)@$t0>Buckets[@$t3]->Name } wchar_t * 0xffff950a'ac39b78a "Protocols" wchar_t * 0xffff950a'ac17b7aa "Topics" wchar_t * 0xffff950a'b2fd282a "TaskbarDPI_Deskband" wchar_t * 0xffff950a'b3e2b47a "Static" wchar_t * 0xffff950a'b3c9458a "SysTreeView32" wchar_t * 0xffff950a'ac34143a "UxSubclassInfo" wchar_t * 0xffff950a'ac5520fa "StdShowItem" wchar_t * 0xffff950a'abc6762a "SysSetRedraw" wchar_t * 0xffff950a'b4a5340a "UIA_WindowVisibilityOverridden" wchar_t * 0xffff950a'ab2c536a "True" ... wchar_t * 0xffff950a'b492c3ea "tooltips_class" wchar_t * 0xffff950a'ac23f46a "Save" wchar_t * 0xffff950a'ac29568a "MSDraw" wchar_t * 0xffff950a'ac54f32a "StdNewDocument" → Anwendungscontainer Kapitel 7 809
wchar_t * 0xffff950a'b546127a "{FB2E3E59-B442-4B5B-9128-2319BF8DE3B0}" wchar_t * 0xffff950a'ac2e6f4a "Status" wchar_t * 0xffff950a'ad9426da "ThemePropScrollBarCtl" wchar_t * 0xffff950a'b3edf5ba "Edit" wchar_t * 0xffff950a'ab02e32a "System" wchar_t * 0xffff950a'b3e6c53a "MDIClient" wchar_t * 0xffff950a'ac17a6ca "StdDocumentName" wchar_t * 0xffff950a'ac6cbeea "StdExit" wchar_t * 0xffff950a'b033c70a "{C56C5799-4BB3-7FAE-7FAD-4DB2F6A53EFF}" wchar_t * 0xffff950a'ab0360fa "MicrosoftTabletPenServiceProperty" wchar_t * 0xffff950a'ac2f8fea "OLEsystem" Fähigkeiten von Anwendungscontainern Wie Sie gesehen haben, verfügen UWP-Anwendungen über stark eingeschränkte Zugriffsrechte. Wie ist es dann beispielsweise Microsoft Edge möglich, das lokale Dateisystem zu durchsuchen und PDF-Dateien im Dokumentenordner des Benutzers zu öffnen? Wie kann die Musikanwendung MP3-Dateien aus dem Musikverzeichnis abspielen? Unabhängig davon, ob die Erlaubnis über Kernelzugriffsprüfungen oder über Broker erfolgt (die Sie im nächsten Abschnitt kennenlernen werden), bilden die Fähigkeits-SIDs den entscheidenden Faktor. Daher wollen wir uns nun ansehen, woher sie kommen, wie sie erstellt und wie sie verwendet werden. Als Erstes müssen UWP-Entwickler ein Anwendungsmanifest mit Angaben wie dem Paketnamen, dem Logo, den Ressourcen, den unterstützten Geräten usw. erstellen. Eines der entscheidenden Elemente für die Verwaltung der Fähigkeiten ist die Fähigkeitenliste in diesem Manifest. Im Manifest von Cortana (%SystemRoot%\SystemApps\Microsoft.Windows.Cortana_ cw5n1h2txywey\AppxManifest.xml) sieht diese Liste wie folgt aus: <Capabilities> <wincap:Capability Name="packageContents"/> <!-- Needed for resolving MRT strings --> <wincap:Capability Name="cortanaSettings"/> <wincap:Capability Name="cloudStore"/> 810 Kapitel 7 Sicherheit
<wincap:Capability Name="visualElementsSystem"/> <wincap:Capability Name="perceptionSystem"/> <Capability Name="internetClient"/> <Capability Name="internetClientServer"/> <Capability Name="privateNetworkClientServer"/> <uap:Capability Name="enterpriseAuthentication"/> <uap:Capability Name="musicLibrary"/> <uap:Capability Name="phoneCall"/> <uap:Capability Name="picturesLibrary"/> <uap:Capability Name="sharedUserCertificates"/> <rescap:Capability Name="locationHistory"/> <rescap:Capability Name="userDataSystem"/> <rescap:Capability Name="contactsSystem"/> <rescap:Capability Name="phoneCallHistorySystem"/> <rescap:Capability Name="appointmentsSystem"/> <rescap:Capability Name="chatSystem"/> <rescap:Capability Name="smsSend"/> <rescap:Capability Name="emailSystem"/> <rescap:Capability Name="packageQuery"/> <rescap:Capability Name="slapiQueryLicenseValue"/> <rescap:Capability Name="secondaryAuthenticationFactor"/> <DeviceCapability Name="microphone"/> <DeviceCapability Name="location"/> <DeviceCapability Name="wiFiControl"/> </Capabilities> Diese Liste enthält verschiedene Arten von Einträgen. Darunter sind die Capability-Einträge, die die Standardkonstanten für die ursprünglichen, in Windows 8 eingeführten Fähigkeiten nennen. Die eigentlichen Konstanten beginnen mit SECURITY_CAPABILITY_, also beispielsweise SECURITY_CAPABILITY_INTERNET_CLIENT, die zum Fähigkeits-RID unter APPLICATION PACKAGE ­AUTHORITY gehört. Diese Konstante ist mit der SID S-1-15-3-1 verknüpft. Andere Einträge weisen Präfixe wie uap, rescap und wincap auf. Bei rescap handelt es sich um eingeschränkte Fähigkeiten, die eine besondere Genehmigung durch Microsoft erfordern, bevor sie im Store zugelassen werden. Im Fall von Cortana sind das Fähigkeiten wie der Zugriff auf SMS-Textnachrichten, E-Mails, Kontakte, Standorte und Benutzerdaten. Windows-Fähigkeiten dagegen (wincap) sind für Windows und Systemanwendungen reserviert und können nicht von Store-Anwendungen genutzt werden. UAP-Fähigkeiten schließlich sind Standardfähigkeiten, die jeder im Store anfordern kann. (Wie bereits erwähnt, ist UAP eine ältere Bezeichnung für UWP.) Die Implementierung erfolgt hier anders als bei den zuerst erwähnten Fähigkeiten, die hartcodierten RIDs zugeordnet sind. Dadurch ist es nicht nötig, eine Liste von Standard-RIDs zu unterhalten. Stattdessen können die Fähigkeiten vollständig benutzerdefiniert sein und im laufenden Betrieb aktualisiert werden. Dazu wird einfach der Fähigkeitsstring in Großbuchstaben umgewandelt und ein SHA-2-Hash davon erstellt (ebenso, wie der SHA-2-Hash des Anwendungscontainer Kapitel 7 811
Paketnamens als Anwendungscontainer-SID verwendet wird). Auch hier umfassen die SHA-2Hashes wieder 32 Bytes, sodass sich für jede Fähigkeit acht RIDs ergeben, die auf den GrundRID ­SECURITY_CAPABILITY_BASE_RID (3) folgen. Des Weiteren gibt es auch einige DeviceCapability-Einträge. Sie geben die Geräteklassen an, auf die die UWP-Anwendung zugreifen muss, und können entweder ebenfalls Standardstrings verwenden oder direkt den GUID der Klasse enthalten. Für diese Art von Fähigkeiten wird die SID nicht durch eine der bereits bekannten, sondern durch eine dritte Methode generiert! Dazu wird der GUID ins Binärformat umgewandelt und dann auf vier RIDs abgebildet (da ein GUID 16 Bytes umfasst). Wurde ein Standardname angegeben, muss dieser zunächst in einen GUID konvertiert werden. Dazu wird im Registrierungsschlüssel HKLM\Software\Microsoft\ Windows\CurrentVersion\DeviceAccess\CapabilityMappings nachgeschlagen, der eine Liste von Registrierungsschlüsseln für Gerätefähigkeiten und eine Liste von GUIDs für diese Fähigkeiten enthält. Eine aktuelle Liste unterstützter Fähigkeiten finden Sie auf https://msdn.microsoft. com/en-us.windows/uwp/packaging/app-capability-declarations. Um all diese Fähigkeiten in das Token aufzunehmen, werden die beiden zusätzlichen Regeln angewendet: QQ Wie Sie in einem früheren Experiment möglicherweise schon gesehen haben, ist in jedem Anwendungscontainertoken die Paket-SID in Form einer Fähigkeit enthalten. Damit kann das Fähigkeitssystem den Zugriff für eine bestimmte Anwendung gezielt durch eine allgemeine Sicherheitsprüfung sperren, anstatt die Paket-SID einzeln abzurufen und zu validieren. QQ Mithilfe des RIDs SECURITY_CAPABILITY_APP_RID (1024) wird jede Fähigkeit wiederum zu einer Gruppen-SID gemacht. Sie dient als Unterautorität, die Vorrang vor den regulären acht Hash-RIDs für die Fähigkeiten hat. Da die Fähigkeiten im Token angegeben sind, können verschiedene Systemkomponenten sie lesen, um zu bestimmen, ob eine Operation, die ein Anwendungscontainer durchführt, erlaubt werden sollte. Die meisten der APIs sind nicht dokumentiert, da Kommunikation und Interoperabilität mit UWP-Anwendung nicht offiziell unterstützt werden und am besten Brokerdiensten, mitgelieferten Treibern und Kernelkomponenten überlassen werden sollten. Beispielsweise können der Kernel und die Treiber die API RtlCapabilityCheck verwenden, um den Zugriff auf bestimmte Hardwareschnittstellen oder APIs zu authentifizieren. Beispielsweise prüft die Energieverwaltung, ob die Fähigkeit ID_CAP_SCREENOFF vorhanden ist, bevor sie die Anforderung eines Anwendungscontainers, den Bildschirm auszuschalten, genehmigt. Der Bluetooth-Porttreiber hält nach der Fähigkeit bluetoothDiagnostics Ausschau und der Anwendungsidentitätstreiber prüft, ob eine EDP-Unterstützung in Form der Fähigkeit enterpriseDataPolicy vorliegt. Im Benutzermodus kann die dokumentierte API CheckTokenCapability verwendet werden, wobei aber nicht der Name der Fähigkeit, sondern ihre SID angegeben werden muss (den jedoch die nicht dokumentierte Funktion RtlDeriveCapabilitySidFromName abrufen kann). Eine weitere Möglichkeit bietet die nicht dokumentierte API CapabilityCheck, die einen String akzeptiert. 812 Kapitel 7 Sicherheit
Viele RPC-Dienste nutzen auch die API RpcClientCapabilityCheck. Diese Hilfsfunktion kümmert sich darum, das Token abzurufen, und benötigt nur den Fähigkeitsstring. Sie wird in vielen WinRT-fähigen Diensten und Brokern verwendet, die RPCs für die Kommunikation mit UWP-Clientanwendungen nutzen. Experiment: Fähigkeiten von Anwendungscontainern einsehen Um die verschiedenen Arten von Fähigkeiten und ihre Aufnahme in ein Token vorzuführen, sehen wir sie uns bei einer vielschichtigen Anwendung wie Cortana an. Deren Manifest kennen Sie bereits und können es daher mit der Benutzeroberfläche vergleichen. Wenn Sie den Inhalt der Registerkarte Security im Eigenschaftendialogfeld für SearchUI.exe nach der Spalte Flags sortieren, erhalten Sie Folgendes: → Anwendungscontainer Kapitel 7 813
Cortana hat eine Menge Fähigkeiten erhalten, nämlich alle aus dem Manifest. Einige gehören zu denen, die mit Windows 8 eingeführt wurden und zu Standardfunktionen wie IsWellKnownSid gehören. Für sie gibt Process Explorer einen Anzeigenamen an. Die anderen Fähigkeiten werden dagegen nur anhand ihrer SIDs bezeichnet, die – wie bereits erklärt – entweder für Hashes oder für GUIDs stehen. Um Angaben zu dem Paket zu erhalten, aus dem der UWP-Prozess erstellt wurde, können Sie das Tool UWPList verwenden, das zusammen mit dem sonstigen Begleitmaterial zu diesem Buch zum Download bereitsteht. Es zeigt alle Vollbildprozesse auf dem System bzw. den einzelnen Prozess, dessen ID Sie angeben: C:\WindowsInternals>UwpList.exe 3728 List UWP Processes - version 1.1 (C)2016 by Pavel Yosifovich Building capabilities map... done. Process ID: 3728 -----------------Image name: C:\Windows\SystemApps\Microsoft.Windows.Cortana_cw5n1h2txyewy\ SearchUI.exe Package name: Microsoft.Windows.Cortana Publisher: CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US Published ID: cw5n1h2txyewy Architecture: Neutral Version: 1.7.0.14393 AppContainer SID: S-1-15-2-1861897761-1695161497-2927542615-642690995327840285-2659745135-2630312742 Lowbox Number: 3 Capabilities: 32 cortanaSettings (S-1-15-3-1024-1216833578-114521899-3977640588-13431805122505059295-473916851-3379430393-3088591068) (ENABLED) visualElementsSystem (S-1-15-3-1024-3299255270-1847605585-2201808924-7104067093613095291-873286183-3101090833-2655911836) (ENABLED) perceptionSystem (S-1-15-3-1024-34359262-2669769421-2130994847-30683386393284271446-2009814230-2411358368-814686995) (ENABLED) internetClient (S-1-15-3-1) (ENABLED) internetClientServer (S-1-15-3-2) (ENABLED) privateNetworkClientServer (S-1-15-3-3) (ENABLED) enterpriseAuthentication (S-1-15-3-8) (ENABLED) musicLibrary (S-1-15-3-6) (ENABLED) phoneCall (S-1-15-3-1024-383293015-3350740429-1839969850-1819881064-15694546864198502490-78857879-1413643331) (ENABLED) → 814 Kapitel 7 Sicherheit
picturesLibrary (S-1-15-3-4) (ENABLED) sharedUserCertificates (S-1-15-3-9) (ENABLED) locationHistory (S-1-15-3-1024-3029335854-3332959268-2610968494-19446639221108717379-267808753-1292335239-2860040626) (ENABLED) userDataSystem (S-1-15-3-1024-3324773698-3647103388-1207114580-21732465724287945184-2279574858-157813651-603457015) (ENABLED) contactsSystem (S-1-15-3-1024-2897291008-3029319760-3330334796-4656416233782203132-742823505-3649274736-3650177846) (ENABLED) phoneCallHistorySystem (S-1-15-3-1024-2442212369-1516598453-23309951313469896071-605735848-2536580394-3691267241-2105387825) (ENABLED) appointmentsSystem (S-1-15-3-1024-2643354558-482754284-283940418-26295591252595130947-547758827-818480453-1102480765) (ENABLED) chatSystem (S-1-15-3-1024-2210865643-3515987149-1329579022-37618428793142652231-371911945-4180581417-4284864962) (ENABLED) smsSend (S-1-15-3-1024-128185722-850430189-1529384825-139260854-3294999511660931883-3499805589-3019957964) (ENABLED) emailSystem (S-1-15-3-1024-2357373614-1717914693-1151184220-28205398343900626439-4045196508-2174624583-3459390060) (ENABLED) packageQuery (S-1-15-3-1024-1962849891-688487262-3571417821-3628679630802580238-1922556387-206211640-3335523193) (ENABLED) slapiQueryLicenseValue (S-1-15-3-1024-3578703928-3742718786-7859573-19308449422949799617-2910175080-1780299064-4145191454) (ENABLED) S-1-15-3-1861897761-1695161497-2927542615-642690995-327840285-26597451352630312742 (ENABLED) S-1-15-3-787448254-1207972858-3558633622-1059886964 (ENABLED) S-1-15-3-3215430884-1339816292-89257616-1145831019 (ENABLED) S-1-15-3-3071617654-1314403908-1117750160-3581451107 (ENABLED) S-1-15-3-593192589-1214558892-284007604-3553228420 (ENABLED) S-1-15-3-3870101518-1154309966-1696731070-4111764952 (ENABLED) S-1-15-3-2105443330-1210154068-4021178019-2481794518 (ENABLED) S-1-15-3-2345035983-1170044712-735049875-2883010875 (ENABLED) S-1-15-3-3633849274-1266774400-1199443125-2736873758 (ENABLED) S-1-15-3-2569730672-1095266119-53537203-1209375796 (ENABLED) S-1-15-3-2452736844-1257488215-2818397580-3305426111 (ENABLED) Die Ausgabe zeigt den vollständigen Paketnamen, das ausführbare Verzeichnis, die Anwendungscontainer-SID, Angaben zum Herausgeber, die Version und die Liste der Fähigkeiten. Des Weiteren ist die LowBox-Nummer angegeben, bei der es sich jedoch lediglich um einen lokalen Index der Anwendung handelt. Im Kerneldebugger können Sie all diese Eigenschaften mit dem Befehl !token untersuchen. Anwendungscontainer Kapitel 7 815
Einige UWP-Anwendungen werden als vertrauenswürdig bezeichnet. Sie verwenden zwar ebenso wie andere UWP-Anwendungen die Windows-Laufzeitplattform, können aber nicht in einem Anwendungscontainer laufen und haben eine Integritätsebene höher als Niedrig. Das übliche Beispiel hierfür ist die App Systemeinstellungen (%SystemRoot\ImmersiveControlPanel\ SystemSettings.exe), was auch sinnvoll ist, da sie in der Lage sein muss, Änderungen am System vorzunehmen, die sich von einem Prozess in einem Anwendungscontainer unmöglich durchführen ließen. Wenn Sie sich das Token dieser Anwendung ansehen, können Sie die drei Attribute PKG, SYSAPPID und PKGHOSTID erkennen, die bestätigen, dass es sich tatsächlich um eine Paketanwendung handelt, auch wenn kein Anwendungscontainertoken vorhanden ist. Der Objektnamensraum von Anwendungscontainern Desktopanwendungen können Kernelobjekte anhand ihrer Namen gemeinsam verwenden. Nehmen wir an, Prozess A ruft CreateEvent(Ex) unter Angabe des Namens MyEvent auf, um ein Ereignisobjekt zu erstellen. Dabei erhält er ein Handle, mit dem er das Ereignis später bearbeiten kann. Nun kann Prozess B in derselben Sitzung ebenfalls CreateEvent(Ex) oder auch OpenEvent mit demselben Namen aufrufen. Sofern Prozess B die passenden Berechtigungen hat (was gewöhnlich der Fall hat, wenn er in derselben Sitzung läuft wie A), erhält er ein weiteres Handle auf dasselbe Ereignisobjekt zurück. Wenn Prozess A jetzt SetEvent für das Ereignisobjekt aufruft, während B in einem Aufruf von WaitForSingleObject für dessen Ereignishandle blockiert ist, wird der Wartethread von B freigegeben, da es sich um dasselbe Ereignisobjekt handelt. Diese gemeinsame Nutzung funktioniert, da benannte Objekte im Objekt-Manager-Verzeichnis Sessions\x\BaseNamedObjects erstellt werden (das Sie in Abb. 7–18 in der Anzeige des Sysinternals-Tools WinObj sehen). Darüber hinaus können Desktopanwendungen Objekte auch von verschiedenen Sitzungen aus gemeinsam nutzen, indem sie dem Namen Global\ voranstellen. Dadurch wird das Objekt im \BaseNamedObjects-Verzeichnis unter dem Verzeichnis für Sitzung 0 erstellt (siehe Abb. 7–18). 816 Kapitel 7 Sicherheit
Abbildung 7–18 Das Verzeichnis des Objekt-Managers für benannte Objekte Der Stammobjektnamensraum von Prozessen in Anwendungscontainern befindet sich in \Sessions\x\AppContainerNamedObjects\<Anwendungscontainer-SID>. Da jeder Anwendungscontainer eine eigene SID hat, gibt es für UWP-Anwendungen keine Möglichkeit, um Kernelobjekte gemeinsam zu nutzen. Prozesse in Anwendungscontainern dürfen auch keine benannten Kernelobjekte im Objektnamensraum von Sitzung 0 erstellen. Abb. 7–19 zeigt das Objekt-Manager-Verzeichnis für die Windows-UWP-App des Taschenrechners. Wenn UWP-Apps Daten gemeinsam nutzen möchten, können sie das über genau definierte Verträge tun, die von der Windows-Laufzeit verwaltet werden. (Weitere Informationen darüber entnehmen Sie bitte der MSDN-Dokumentation.) Eine gemeinsame Nutzung von Kernelobjekten durch Desktop- und UWP-Anwendungen ist möglich und erfolgt oft mithilfe von Brokerdiensten. Wenn eine UWP-Anwendung beispielsweise vom Dateiauswahl-Broker den Zugriff auf eine Datei im Dokumentenordner anfordert (und die entsprechende Fähigkeit dazu überprüft wird), erhält sie ein Dateihandle, das sie direkt für Lese- und Schreibvorgänge einsetzen kann, ohne den Zusatzaufwand für das Marshalling der Anforderungen in beiden Richtungen in Kauf nehmen zu müssen. Dazu dupliziert der Broker das Dateihandle in der Handletabelle der UWP-Anwendung. (Mehr über die Duplizierung von Handles erfahren Sie in Kapitel 8 von Band 2.) Zur weiteren Vereinfachung ermöglicht das ALPC-Teilsystem (das ebenfalls in Kapitel 8 beschrieben wird) eine solche automatische Übertragung von Handles mithilfe von ALPC-Handleattributen. Die RPC-Dienste, die ALPC als zugrunde liegendes Protokoll nutzen, können diese Funktionalität Anwendungscontainer Kapitel 7 817
in ihren Schnittstellen verwenden. Marshalling-fähige Handles in der IDL-Datei werden automatisch auf diese Weise durch das ALPC-Teilsystem übertragen. Abbildung 7–19 Das Verzeichnis des Objekt-Managers für die Taschenrechner-Anwendung Außerhalb der offiziellen RPC-Brokerdienste kann eine Desktopanwendung ein benanntes (oder sogar ein unbenanntes) Objekt auf die übliche Weise erstellen und dann mit der Funktion DuplicateHandle manuell ein Handle zu diesem Objekt in den UWP-Prozess injizieren. Das ist möglich, da Desktopanwendungen gewöhnlich auf mittlerer Integritätsebene ausgeführt werden und es nichts gibt, was sie daran hindern könnte, Handles in UWP-Prozesse zu duplizieren. Anders herum dagegen geht es nicht. Gewöhnlich ist keine Kommunikation zwischen einer Desktop- und einer UWP-Anwendung erforderlich, da eine App aus dem Store keine begleitende Desktopanwendung haben und sich daher nicht darauf verlassen kann, dass eine solche Anwendung auf dem Gerät vorhanden ist. Die Fähigkeit, Handles in eine UWP-App zu injizieren, kann in besonderen Fällen erforderlich sein, etwa bei der Verwendung einer Desktop-Bridge (Centennial), um eine Desktop- in eine UWP-Anwendung umzuwandeln und mit einer anderen, bekanntermaßen vorhandenen Desktopanwendung zu kommunizieren. Handles von Anwendungscontainern Für typische Win32-Anwendungen garantiert das Windows-Teilsystem, dass sowohl ein sitzungseigenes als auch das globale BaseNamedObjects-Verzeichnis vorhanden ist. Das Verzeichnis AppContainerBaseNamedObjects wird aber leider von der Startanwendung angelegt. 818 Kapitel 7 Sicherheit
Bei der UWP-Anwendung ist dies der vertrauenswürdige Dienst DComLaunch, doch nicht alle Anwendungscontainer sind unbedingt mit UWP verknüpft, sondern können mithilfe entsprechender Prozesserstellungsattribute auch manuell angelegt werden. (Was für Attribute das sind, erfahren Sie in Kapitel 3.) In einem solchen Fall ist es möglich, dass eine nicht vertrauenswürdige Anwendung das Objektverzeichnis (und die erforderlichen symbolischen Verknüpfungen darin) erstellt hat. Das wiederum kann dazu führen, dass die Startanwendung in der Lage ist, Handles von einer Ebene unterhalb der Containeranwendung aus zu schließen. Selbst wenn keine böse Absicht vorliegt, kann es sein, dass die ursprüngliche Startanwendung beendet wird, ihre Handles aufräumt und dabei das Objektverzeichnis für den Anwendungscontainer zerstört. Um das zu verhindern, kann in einem Anwendungscontainertoken ein Array aus Handles gespeichert werden, deren Existenz für die gesamte Lebensdauer jeglicher Anwendungen garantiert ist, die das Token nutzen. Diese Handles werden übergeben, wenn das Token erstellt wird (mit NtCreateLowBoxToken), und als Kernelhandles dupliziert. Es wird auch eine besondere SEP_CACHED_HANDLES_ENTRY-Struktur verwendet (vergleichbar mit den Atomtabellen der einzelnen Anwendungscontainer). Sie basiert auf einer Hashtabelle, die in der Struktur für die Anmeldesitzung des Benutzers gespeichert ist (siehe den Abschnitt »Anmeldung« weiter hinten in diesem Kapitel). Diese Struktur enthält ein Array aus Kernelhandles, die bei der Erstellung des Anwendungscontainertokens dupliziert wurden. Geschlossen werden diese Handles, wenn das Token zerstört wird (da die Anwendung beendet wird) oder sich der Benutzer abmeldet (wobei die Anmeldesitzung endet). Experiment: Im Token gespeicherte Handles einsehen Um sich die im Token gespeicherten Handles anzusehen, gehen Sie wie folgt vor: 1. Starten Sie die Taschenrechneranwendung und eine lokale Kerneldebuggersitzung. 2. Suchen Sie nach dem Prozess des Taschenrechners: lkd> !process 0 1 calculator.exe PROCESS ffff828cc9ed1080 SessionId: 1 Cid: 4bd8 Peb: d040bbc000 ParentCid: 03a4 DeepFreeze DirBase: 5fccaa000 ObjectTable: ffff950ad9fa2800 HandleCount: <Data Not Accessible> Image: Calculator.exe VadRoot ffff828cd2c9b6a0 Vads 168 Clone 0 Private 2938. Modified 3332. Locked 0. DeviceMap ffff950aad2cd2f0 Token ffff950adb313060 ElapsedTime 1 Day 08:01:47.018 UserTime 00:00:00.015 KernelTime 00:00:00.031 QuotaPoolUsage[PagedPool] 465880 → Anwendungscontainer Kapitel 7 819
QuotaPoolUsage[NonPagedPool] Working Set Sizes (now,min,max) 23288 (7434, 50, 345) (29736KB, 200KB, 1380KB) 3. PeakWorkingSetSize 11097 VirtualSize 303 Mb PeakVirtualSize 314 Mb PageFaultCount 21281 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 4925 Job ffff828cd4914060 Geben Sie das Token mithilfe des Befehls dt aus. (Denken Sie daran, dass Sie die unteren drei oder vier Bits maskieren müssen, wenn sie nicht null sind.) lkd> dt nt!_token ffff950adb313060 +0x000 TokenSource : _TOKEN_SOURCE +0x010 TokenId : _LUID +0x018 AuthenticationId : _LUID +0x020 ParentTokenId : _LUID ... +0x0c8 TokenFlags : 0x4a00 +0x0cc TokenInUse : 0x1 '' +0x0d0 IntegrityLevelIndex : 1 +0x0d4 MandatoryPolicy : 1 +0x0d8 LogonSession : 0xffff950a'b4bb35c0 _SEP_LOGON_SESSION_ REFERENCES +0x0e0 OriginatingLogonSession : _LUID +0x0e8 SidHash : _SID_AND_ATTRIBUTES_HASH +0x1f8 RestrictedSidHash : _SID_AND_ATTRIBUTES_HASH +0x308 pSecurityAttributes : 0xffff950a'e4ff57f0 _AUTHZBASEP_SECURITY_ ATTRIBUTES_INFORMATION +0x310 Package : 0xffff950a'e00ed6d0 Void +0x318 Capabilities : 0xffff950a'e8e8fbc0 _SID_AND_ATTRIBUTES +0x320 CapabilityCount : 1 +0x328 CapabilitiesHash : _SID_AND_ATTRIBUTES_HASH +0x438 LowboxNumberEntry : 0xffff950a'b3fd55d0 _SEP_LOWBOX_NUMBER_ENTRY +0x440 LowboxHandlesEntry : 0xffff950a'e6ff91d0 _SEP_LOWBOX_HANDLES_ENTRY +0x448 pClaimAttributes : (null) ... → 820 Kapitel 7 Sicherheit
4. Geben Sie das Element LowboxHandlesEntry aus: lkd> dt nt!_sep_lowbox_handles_entry 0xffff950a'e6ff91d0 +0x000 HashEntry : _RTL_DYNAMIC_HASH_TABLE_ENTRY +0x018 ReferenceCount : 0n10 5. +0x020 PackageSid : 0xffff950a'e6ff9208 Void +0x028 HandleCount : 6 +0x030 Handles : 0xffff950a'e91d8490 -> 0xffffffff'800023cc Void Es gibt sechs Handles, deren Werte wir als Nächstes ausgeben: lkd> dq 0xffff950ae91d8490 L6 ffff950a'e91d8490 ffffffff'800023cc ffffffff'80001e80 ffff950a'e91d84a0 ffffffff'80004214 ffffffff'8000425c ffff950a'e91d84b0 ffffffff'800028c8 ffffffff'80001834 6. Wie Sie sehen, handelt es sich durchweg um Kernelhandles, da ihre Werte alle mit 0xffffffff beginnen (64 Bit). Mit dem Befehl !handle können Sie sich jetzt die einzel- nen Handles ansehen. Die folgende Ausgabe zeigt zwei der sechs zuvor angegebenen Handles: lkd> !handle ffffffff'80001e80 PROCESS ffff828cd71b3600 SessionId: 1 Cid: 27c4 Peb: 3fdfb2f000 ParentCid: 2324 DirBase: 80bb85000 ObjectTable: ffff950addabf7c0 HandleCount: <Data Not Accessible> Image: windbg.exe Kernel handle Error reading handle count. 80001e80: Object: ffff950ada206ea0 GrantedAccess: 0000000f (Protected) (Inherit) (Audit) Entry: ffff950ab5406a00 Object: ffff950ada206ea0 Type: (ffff828cb66b33b0) Directory ObjectHeader: ffff950ada206e70 (new version) HandleCount: 1 PointerCount: 32770 Directory Object: ffff950ad9a62950 Name: RPC Control Hash Address Type Name ---- ------- ---- ---- 23 ffff828cb6ce6950 ALPC Port OLE376512B99BCCA5DE4208534E7732 lkd> !handle ffffffff'800028c8 → Anwendungscontainer Kapitel 7 821
PROCESS ffff828cd71b3600 SessionId: 1 Cid: 27c4 Peb: 3fdfb2f000 ParentCid: 2324 DirBase: 80bb85000 ObjectTable: ffff950addabf7c0 HandleCount: <Data Not Accessible> Image: windbg.exe Kernel handle Error reading handle count. 800028c8: Object: ffff950ae7a8fa70 GrantedAccess: 000f0001 (Audit) Entry: ffff950acc426320 Object: ffff950ae7a8fa70 Type: (ffff828cb66296f0) SymbolicLink ObjectHeader: ffff950ae7a8fa40 (new version) HandleCount: 1 PointerCount: 32769 Directory Object: ffff950ad9a62950 Name: Session Flags: 00000000 ( Local ) Target String is '\Sessions\1\AppContainerNamedObjects \S-1-15-2-466767348-3739614953-2700836392-1801644223-4227750657-10878335352488631167' Da die Möglichkeit, benannte Objekte auf einen bestimmten Objektverzeichnis-Namensraum zu beschränken, eine wertvolle Sicherheitsvorkehrung im Rahmen einer Sandbox für den Zugriff auf benannte Objekte darstellt, enthält das Creators Update von Windows 10 die zusätzliche Tokenfähigkeit der BNO-Isolierung (wobei BNO für BaseNamedObjects steht). Unter Verwendung der gleichen SEP_CACHE_HANDLES_ENTRY-Struktur wird der Tokenstruktur das neue Feld BnoIsolationHandlesEntry hinzugefügt, das jedoch den neuen Typ SepCachedHandles­ EntryBnoIsolation und nicht SepCachedHandlesEntryLowbox hat. Um diese Einrichtung nutzen zu können, ist ein besonderes Prozessattribut (siehe Kapitel 3) mit einem Isolationspräfix und einer Handleliste erforderlich. Dabei wird zwar der gleiche LowBox-Mechanismus eingesetzt wie zuvor, allerdings wird statt des Objektverzeichnisses für die Anwendungscontainer-SID ein Verzeichnis mit dem Präfix aus dem Attribut verwendet. Broker Da Anwendungscontainer fast keine Berechtigungen außer denen haben, die implizit über die Fähigkeiten gewährt wurden, können sie einige übliche Operationen nicht direkt durchführen, sondern benötigen dazu Hilfe. (Für diese Operationen gibt es keine Fähigkeiten, da sie zu maschinennah sind, um sie den Benutzern im Store anzuzeigen, und sich nur schwer verwalten lassen.) Dazu gehören etwa die Auswahl von Dateien mithilfe des üblichen Dialogfelds Datei öffnen oder das Drucken mit dem entsprechenden Dialogfeld. Für diese und ähnliche Vorgänge bietet Windows Hilfsprozesse, die als Broker bezeichnet und vom Systembrokerprozess Run­ timeBroker.exe verwaltet werden. 822 Kapitel 7 Sicherheit
Ein Anwendungscontainerprozess, der einen dieser Dienste benötigt, teilt dies dem Laufzeit-Broker über einen sicheren ALPC-Kanal mit. Der Laufzeit-Broker sorgt anschließend dafür, dass der angeforderte Brokerprozess erstellt wird, also z. B. %SystemRoot%\PrintDialog\PrintDialog.exe oder %SystemRoot%\System32\PickerHost.exe. Experiment: Broker Die folgenden Schritte zeigen, wie Brokerprozesse gestartet und beendet werden: 1. Klicken Sie auf Start, geben Sie Fotos ein und wählen Sie die Option Fotos aus, um die im Lieferumfang von Windows enthaltene Anwendung Fotos zu starten. 2. Öffnen Sie Process Explorer, schalten Sie die Prozessliste in die Baumansicht und suchen Sie den Prozess Microsoft.Photos.exe. Platzieren Sie beide Fenster nebeneinander. 3. Wählen Sie in Fotos eine Bilddatei aus, rechtsklicken Sie darauf oder öffnen Sie das Menü mit den drei Punkten oben rechts und wählen Sie Drucken. Dadurch wird das Dialogfeld Drucken geöffnet. In Process Explorer sehen Sie jetzt den neu erstellten Broker (PrintDialog.exe). Wie Sie sehen, sind all diese Prozesse Kinder desselben Svchost-Prozesses. (Alle UWP-Prozesse werden von dem darin befindlichen Dienst DCOMLaunch gestartet.) 4. Schließen Sie das Dialogfeld Drucken. Der Prozess PrintDialog.exe wird beendet. Anwendungscontainer Kapitel 7 823
Anmeldung Bei der interaktiven Anmeldung (im Gegensatz zur Anmeldung über das Netzwerk) wirken die folgenden Komponenten zusammen: Der Anmeldeprozess (Winlogon.exe) QQ Der Prozess der Anmeldeschnittstelle (LogonUI.exe) und seine Anmeldeinformationsan­ bieter QQ Lsass.exe QQ Ein oder mehrere Authentifizierungspakete QQ SAM oder Active Directory Authentifizierungspakete sind DLLs, die Authentifizierungsprüfungen durchführen. Für die interaktive Anmeldung an einer Domäne verwendet Windows das Authentifizierungspaket Kerberos. Für die interaktive Anmeldung am lokalen Computer, für Anmeldungen an vertrauenswürdigen Prä-Windows 2000-Domänen und für Situationen, in denen kein Domänencontroller zugänglich ist, wird dagegen das Paket MSV1_0 eingesetzt. Winlogon ist ein vertrauenswürdiger Prozess für die Verwaltung sicherheitsrelevanter Benutzervorgänge. Er koordiniert die Anmeldung, startet den ersten Prozess des Benutzers bei der Abmeldung und kümmert sich um die Abmeldung. Außerdem verwaltet er verschiedene andere sicherheitsrelevante Operationen, darunter den Start von LogonUI zur Eingabe von Kennwörtern bei der Anmeldung, die Änderung von Kennwörtern und das Sperren und Entsperren der Arbeitsstation. Der Winlogon-Prozess muss dafür sorgen, dass sicherheitsrelevante Operationen für andere aktive Prozesse nicht sichtbar sind. Beispielsweise garantiert Winlogon, dass ein nicht vertrauenswürdiger Prozess während dieser Operationen keine Kontrolle über den Desktop und damit auch keinen Zugriff auf das Kennwort erlangen kann. Um den Kontonamen und das Kennwort eines Benutzers zu erlangen, stützt sich Winlogon auf die Anmeldeinformationsanbieter, die auf dem System installiert sind. Bei diesen Anbietern handelt es sich um COM-Objekte innerhalb von DLLs. Die Standardanbieter sind Authui.dll, SmartcardCredentialProvider.dll und FaceCredentialProvider.dll, die die Authentifizierung über ein Kennwort, eine Smartcard-PIN bzw. die Gesichtserkennung ermöglichen. Durch die Installation zusätzlicher Anmeldeinformationsanbieter kann Windows auch weitere Identifizierungsmechanismen nutzen. Beispielsweise kann ein Dritthersteller einen Anbieter hinzufügen, der den Benutzer anhand eines Fingerabdruckgeräts erkennt und das zugehörige Kennwort dann aus einer verschlüsselten Datenbank entnimmt. Die vorhandenen Anmeldeinformationsanbieter sind unter HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers aufgeführt, wobei jeder Unterschlüssel eine Anbieterklasse anhand ihrer COM-CLSID bezeichnet. (Die CLSID muss dabei wie jede andere COM-Klasse unter HKCR\CLSID registriert sein.) Mit dem Werkzeug CPlist.exe aus dem Begleitmaterial zu diesem Buch können Sie sich die Anmeldeinformationsanbieter mit ihrer CLSID, ihrem Anzeigenamen und der DLL mit ihrer Implementierung anzeigen lassen. Um den Adressraum von Winlogon gegen Bugs in Anmeldeinformationsanbietern zu schützen, die zu einem Absturz des Prozesses führen könnten (und damit zu einem Systemab- 824 Kapitel 7 Sicherheit
sturz, da Winlogon ein kritischer Systemprozess ist), wird zum eigentlichen Laden der Anbieter und zur Anzeige der Anmeldeoberfläche für die Benutzer ein zweiter Prozess verwendet, nämlich LogonUI.exe. Er wird bei Bedarf gestartet, wenn Winlogon eine Anmeldeoberfläche anzeigen muss, und nach Abschluss dieses Vorgangs wieder beendet. Sollte ein LogonUI-Prozess aus irgendeinem Grunde abstürzen, kann Winlogon daher ganz einfach einen neuen starten. Winlogon ist der einzige Prozess, der Anmeldeanforderungen von der Tastatur abfängt, die als RPC-Nachrichten von Win32k.sys gesendet werden. Er startet sofort die Anwendung LogonUI, um die Benutzeroberfläche für die Anmeldung anzuzeigen. Nachdem Winlogon den Benutzernamen und das Kennwort vom Anmeldeinformationsanbieter in Erfahrung gebracht hat, ruft er Lsass auf, um den Benutzer zu authentifizieren. Wenn das gelingt, aktiviert der Anmeldeprozess eine Anmeldeshell für den Benutzer. Das Zusammenspiel der verschiedenen Komponenten sehen Sie in Abb. 7–20. NetLogonDienst LSA_Server Netzwerk Dienst des Kerberos-Schlüssel­ verteilungs­centers (Andere) Anmelde­informa­ tions­anbieter-DLL Active Directory-Server SAM-Server Abbildung 7–20 An der Anmeldung beteiligte Komponenten Es gibt nicht nur die Möglichkeit, weitere Anmeldeinformationsanbieter zu nutzen. LogonUI kann auch zusätzliche Netzwerkanbieter-DLLs für eine sekundäre Authentifizierung laden. Dadurch können mehrere Netzwerkanbieter zusammenarbeiten, um Identifizierungs- und Authentifizierungsinformationen während der normalen Anmeldung zu erfassen. Dadurch kann ein Benutzer, der sich an einem Windows-System anmeldet, gleichzeitig auf einem Linux-Server authentifiziert werden, und anschließend von seinem Rechner aus auf Ressourcen auf diesem Server zugreifen, ohne sich erneut authentifizieren zu müssen. Dies wird als »einmalige Anmeldung« (Single Sign-On) bezeichnet. Anmeldung Kapitel 7 825
Initialisierung durch Winlogon Während der Systeminitialisierung, noch bevor irgendwelche Benutzeranwendungen aktiviert werden, führt Winlogon die folgenden Schritte aus, um dafür zu sorgen, dass er die Kontrolle über die Arbeitsstation hat, wenn das System bereit zur Interaktion mit dem Benutzer ist: 1. Er erstellt und öffnet eine interaktive Fensterstation (z. B. Sessions\1\Windows\WindowStations\WinSta0 im Namensraum des Objekt-Managers) für die Tastatur, die Maus und den Monitor. Außerdem legt Winlogon für diese Station einen Sicherheitsdeskriptor mit genau einem Zugriffssteuerungseintrag an, der nur die System-SID enthält. Dieser besondere Sicherheitsdeskriptor sorgt dafür, dass andere Prozesse nur dann auf die Arbeitsstation zugreifen können, wenn Winlogon dies ausdrücklich erlaubt. 2. Er erstellt und öffnet einen Anwendungsdesktop (\Sessions\1\Windows\WinSta0\ Default, auch als »interaktiver Desktop« bezeichnet) und einen Winlogon-Desktop (\Sessions\1\ Windows\WinSta0\Winlogon, »sicherer Desktop«). Die Sicherheit des Winlogon-Desktops wird so eingestellt, dass nur Winlogon darauf zugreifen kann. Der andere Desktop dagegen ist sowohl für Winlogon als auch für Benutzer zugänglich. Wenn der Winlogon-Desktop aktiv ist, hat daher kein anderer Prozess Zugriff auf irgendwelchen aktiven Code oder Daten, die mit dem Desktop verknüpft sind. Damit schützt Winlogon die sicherheitsrelevanten Vorgänge im Zusammenhang mit Kennwörtern und mit dem Sperren und Entsperren des Desktops. 3. Bevor sich irgendjemand am Computer anmeldet, ist nur der Winlogon-Desktop sicher. Wenn ein Benutzer angemeldet ist, führt ein Sicherheitsaufruf (Secure Attention Sequence, SAS), also die Betätigung von (Strg) + (Alt) + (Entf) dazu, dass wieder vom Standardzum Winlogon-Desktop umgeschaltet und LogonUI gestartet wird. (Das erklärt auch, warum alle Fenster auf dem interaktiven Desktop zu verschwinden scheinen, wenn Sie die Tastenkombination (Strg) + (Alt) + (Entf) drücken, und nach dem Schließen des Windows-Sicherheitsdialogfelds wieder erscheinen.) Der Sicherheitsaufruf führt also immer zu einem von Winlogon gesteuerten sicheren Desktop. 4. Er richtet eine ALPC-Verbindung mit Lsass ein, die für den Austausch von Informationen beim Anmelden, beim Abmelden und bei Passwortoperationen verwendet wird. Erstellt wird sie durch einen Aufruf von LsaRegisterLogonProcess. 5. Er registriert den Winlogon-RPC-Nachrichtenserver, der auf Nachrichten von Win32k über Sicherheitsaufrufe, Abmeldungen und das Sperren der Arbeitsstation lauscht. Dadurch wird verhindert, dass trojanische Pferde bei der Eingabe des Sicherheitsaufrufs Zugriff auf den Bildschirm gewinnen können. Der Prozess Wininit führt Schritt 1 und 2 in ähnlicher Weise aus, um älteren interaktiven Diensten in Sitzung 0 zu erlauben, Fenster anzuzeigen. Die weiteren Schritte jedoch fallen weg, da Sitzung 0 nicht für die Benutzeranmeldung zur Verfügung steht. 826 Kapitel 7 Sicherheit
Die Implementierung des Sicherheitsaufrufs Der Sicherheitsaufruf ist deshalb sicher, da keine Anwendung die Tastenkombination (Strg) + (Alt) + (Entf) abfangen oder Winlogon daran hindern kann, sie zu empfangen. Win32k.sys reserviert diese Tastenkombination, sodass das Windows-Eingabesystem (das im Roh­eingabethread in Win32k implementiert ist) eine RPC-Nachricht an den Nachrichtenserver von Winlogon sendet, sobald es diese Kombination wahrnimmt. Die Ausführung einer registrierten Tastenkombination wird ausschließlich an den Prozess gesendet, der sie registriert hat, und nur der registrierende Thread kann diese Registrierung auch wieder aufheben. Damit ist es trojanischen Pferden nicht möglich, die Registrierung des Sicherheitsaufrufs für Winlogon zu ändern. Mit der Windows-Funktion SetWindowsHookEx kann eine Anwendung eine Hookprozedur installieren, die bei jeder Betätigung einer Taste noch vor der Verarbeitung von Tastenkombinationen aufgerufen wird. Dadurch kann der Hook Tastenkombinationen unterdrücken. Allerdings enthält der Windows-Code zur Verarbeitung von Tastenkombinationen einen Sonderfall für (Strg) + (Alt) + (Entf), der Hooks deaktiviert, sodass die Tastenfolge nicht abgefangen werden kann. Wenn der interaktive Desktop gesperrt ist, werden überdies nur Tastenkombinationen verarbeitet, die Winlogon gehören. Sobald der Winlogon-Desktop bei der Initialisierung erstellt ist, wird er zum aktiven Desktop gemacht. Ist er aktiv, so ist er stets gesperrt. Winlogon entsperrt seinen Desktop nur, um zum Anwendungsdesktop oder dem Bildschirmschonerdesktop umzuschalten. (Nur der Winlogon-Prozess kann einen Desktop sperren und entsperren.) Die einzelnen Schritte der Benutzeranmeldung Die Anmeldung beginnt, wenn ein Benutzer (Strg) + (Alt) + (Entf) drückt. Daraufhin startet Winlogon die Anwendung LogonUI, die wiederum die Anmeldeinformationsanbieter aufruft, um den Benutzernamen und das Kennwort abzurufen. Außerdem erstellt Winlogon eine eindeutige Anmelde-SID für den Benutzer und ordnet sie der aktuellen Instanz des Desktops zu (Tastatur, Bildschirm und Maus). Beim Aufruf von LsaLogonUser übergibt Winlogon diese SID an Lsass. Wird der Benutzer erfolgreich angemeldet, so wird diese SID in das Anmeldeprozesstoken aufgenommen. Das geschieht, um den Zugriff auf den Desktop abzusichern, denn dadurch wird verhindert, dass eine Anmeldung am selben Konto, aber auf einem anderen Computer, auf dem Desktop des ersten Rechners schreibt, weil die SID dieser zweiten Anmeldung nicht im Desktoptoken der ersten Anmeldung vorhanden ist. Nach der Eingabe von Benutzername und Kennwort ruft Winlogon die Lsass-Funktion LsaLookupAuthenticationPackage ab, um ein Handle zu einem Authentifizierungspaket zu gewinnen. Diese Pakete sind in der Registrierung unter HKLM\SYSTEM\CurrentControlSet\Control\ Lsa verzeichnet. Über LsaLogonUser übergibt Winlogon die Anmeldeinformationen an das Authentifizierungspaket. Wenn das Paket den Benutzer authentifiziert hat, setzt Winlogon den Anmeldeprozess für diesen Benutzer fort. Ist mit keinem der Authentifizierungspakete eine erfolgreiche Anmeldung möglich, wird der Anmeldevorgang abgebrochen. Anmeldung Kapitel 7 827
Für die interaktive Anmeldung mit Benutzername und Kennwort verwendet Windows die beiden folgenden Standard-Authentifizierungspakete: QQ MSV1_0 Das Standard-Authentifizierungspaket auf eigenständigen Windows-Systemen ist MSV1_0 (Msv1_0.dll). Es implementiert das Protokoll LAN Manager 2. Lsass verwendet MSV1_0 auch auf Computern in einer Domäne, um Prä-Windows 2000-Domänen und -Computer zu authentifizieren, die keinen Domänencontroller ansprechen können. (Letzteres gilt auch für Computer, die vom Netzwerk getrennt sind.) QQ Kerberos Das Authentifizierungspaket Kerberos.dll wird auf Computern verwendet, die Mitglieder von Windows-Domänen sind. Zusammen mit den Kerberos-Diensten, die auf einem Domänencontroller laufen, unterstützt dieses Paket das Protokoll Kerberos, das auf RFC 1510 basiert. (Mehr Informationen über den Kerberos-Standard erhalten Sie auf der Website der Internet Engineering Task Force auf http://www.ietf.org.) MSV1_0 Das Authentifizierungspaket MSV1_0 nimmt den Benutzernamen und die Hashversion des Kennworts entgegen und sendet eine Anforderung an den lokalen SAM, um die Kontoinformationen einschließlich des Kennworthashs, der Gruppen des Benutzers und eventueller Kontoeinschränkungen abzurufen. Als Erstes prüft MSV1_0 die Kontoeinschränkungen, z. B. Zeiträume oder Arten des erlaubten Zugriffs. Wenn sich der Benutzer aufgrund von Einschränkungen in der SAM-Datenbank nicht anmelden kann, schlägt der Anmeldeaufruf fehl. In diesem Fall gibt MSV1_0 einen Fehlerstatus an die LSA zurück. Anschließend vergleicht MSV1_0 den Kennworthash und den Benutzernamen mit den Versionen, die es vom SAM erhalten hat. Bei einer zwischengespeicherten Domänenanmeldung greift MSV1_0 mithilfe der Lsass-Funktionen zum Speichern und Abrufen von »Geheimnissen« in der LSA-Datenbank (dem Registrierungshive SECURITY) auf die zwischengespeicherten Informationen zu. Wenn die Angaben übereinstimmen, legt MSV1_0 einen LUID für die Anmeldesitzung an, erstellt die Sitzung durch einen Aufruf von Lsass, wobei es den Bezeichner mit der Sitzung verknüpft und die erforderlichen Informationen übergibt, um ein Zugriffstoken für den Benutzer zu generieren. (Wie bereits erwähnt, enthält ein Zugriffstoken die Benutzer-SID, die Gruppen-SIDs und die gewährten Rechte.) MSV1_0 speichert nicht den gesamten Kennworthash in der Registrierung zwischen, da eine Person mit physischem Zugang zu dem System sonst leichtes Spiel hätte, das Domänenkonto des Benutzers zu knacken und Zugriff auf verschlüsselte Daten und Netzwerkressourcen zu erhalten, für die dieser Benutzer autorisiert ist. Stattdessen wird nur der halbe Hash gespeichert. Das reicht aus, um das Kennwort zu überprüfen, aber nicht, um Zugriff auf EFS-Schlüssel zu erhalten, den Benutzer in der Domäne zu authentifizieren, da dafür der vollständige Hash erforderlich ist. 828 Kapitel 7 Sicherheit
Wenn MSV1_0 eine Authentifizierung über ein Remotesystem vornehmen muss – etwa wenn sich ein Benutzer an einer vertrauenswürdigen Prä-Windows 2000-Domäne anmeldet –, kommuniziert es über den Netlogon-Dienst mit einer Netlogon-Instanz auf dem Remotesystem. Diese Instanz gibt die Authentifizierungsergebnisse an das System zurück, auf dem die Anmeldung stattfindet. Kerberos Der grundlegende Ablauf der Kerberos-Authentifizierung ist identisch mit dem von MSV1_0. Allerdings erfolgen Domänenanmeldungen meistens auf Arbeitsstationen oder Servern, die Mitglied der Domäne sind, und nicht auf Domänencontrollern. Daher muss das Authentifizierungspaket über den Kerberos-TCP/IP-Port (Port 88) mit dem Kerberos-Dienst auf dem Domänencontroller kommunizieren. Der Dienst für das Kerberos-Schlüsselverteilungscenter (Key Distribution Center, KDC; Kdcsvc.dll), der das Kerberos-Protokoll implementiert, wird im Lsass-Prozess auf Domänencontrollern ausgeführt. Nach der Überprüfung der Hashes für Benutzername und Kennwort anhand der Benutzerkontoobjekte in Active Directory (mithilfe von Ntdsa.dll auf dem Active Directory-Server) gibt Kdcsvc die Domänenanmeldeinformationen an Lsass zurück, der wiederum das Ergebnis der Authentifizierung und die Anmeldeinformationen (bei erfolgreicher Anmeldung) über das Netzwerk an das System zurückgibt, von dem aus die Anmeldung erfolgt. Diese Beschreibung der Kerberos-Authentifizierung ist stark vereinfacht, macht aber die Rollen der einzelnen Komponenten deutlich. Das Authentifizierungsprotokoll Kerberos spielt eine entscheidende Rolle für die verteilte Domänensicherheit in Windows, doch eine ausführliche Beschreibung würde den Rahmen dieses Buches sprengen. Nach erfolgter Authentifizierung schaut Lsass in der lokalen Richtliniendatenbank nach, welche Art des Zugriffs dem Benutzer erlaubt ist, also interaktiver, Netzwerk-, Batch- oder Dienstprozesszugriff. Entspricht der angeforderte Zugriff nicht dem, was zugelassen ist, wird der Anmeldeversuch beendet. Um neu erstellte Anmeldesitzungen zu löschen, räumt Lsass die Daten­ strukturen auf und gibt einen Fehler an Winlogon zurück, der wiederum eine entsprechende Meldung für den Benutzer anzeigt. Ist der angeforderte Zugriff aber erlaubt, fügt Lsass die entsprechenden zusätzlichen SIDs (z. B. für Jeder, für den interaktiven Zugriff usw.) hinzu, sucht in der Richtliniendatenbank nach den Rechten, die dem Benutzer für diese SIDs gewährt sind, und fügt diese Rechte zum Zugriffstoken des Benutzers hinzu. Nachdem Lsass alle erforderlichen Informationen gesammelt hat, ruft er die Exekutive auf, die das primäre Zugriffstoken für eine interaktive oder Dienstanmeldung und ein Identitätswechseltoken für eine Netzwerkanmeldung erstellt. Anschließend dupliziert Lsass das Token, generiert ein Handle für Winlogon und schließt sein eigenes Handle. Bei Bedarf wird die Anmeldeoperation auch überwacht. Wenn der Vorgang bis zu dieser Stelle gediehen ist, gibt Lsass die Erfolgmeldung, das Handle für das Zugriffstoken, den LUID für die Anmeldesitzung und ggf. die Profilinformationen, die er vom Authentifizierungspaket erhalten hat, an Winlogon zurück. Anmeldung Kapitel 7 829
Experiment: Aktive Anmeldesitzungen auflisten Sofern es für den LUID einer Anmeldesitzung mindestens ein Token gibt, sieht Windows die Sitzung als aktiv an. Mit dem Sysinternals-Tool LogonSessions, das die im Windows SDK dokumentierte Funktion LsaEnumerateLogonSessions nutzt, können Sie sich die aktiven Anmeldesitzungen anzeigen lassen: C:\WINDOWS\system32>logonsessions LogonSessions v1.4 - Lists logon session information Copyright (C) 2004-2016 Mark Russinovich Sysinternals - www.sysinternals.com [0] Logon session 00000000:000003e7: User name: WORKGROUP\ZODIAC$ Auth package: NTLM Logon type: (none) Session: 0 Sid: S-1-5-18 Logon time: 09-Dec-16 15:22:31 Logon server: DNS Domain: UPN: [1] Logon session 00000000:0000cdce: User name: Auth package: NTLM Logon type: (none) Session: 0 Sid: (none) Logon time: 09-Dec-16 15:22:31 Logon server: DNS Domain: UPN: [2] Logon session 00000000:000003e4: User name: WORKGROUP\ZODIAC$ Auth package: Negotiate Logon type: Service Session: 0 Sid: S-1-5-20 Logon time: 09-Dec-16 15:22:31 → 830 Kapitel 7 Sicherheit
Logon server: DNS Domain: UPN: [3] Logon session 00000000:00016239: User name: Window Manager\DWM-1 Auth package: Negotiate Logon type: Interactive Session: 1 Sid: S-1-5-90-0-1 Logon time: 09-Dec-16 15:22:32 Logon server: DNS Domain: UPN: [4] Logon session 00000000:00016265: User name: Window Manager\DWM-1 Auth package: Negotiate Logon type: Interactive Session: 1 Sid: S-1-5-90-0-1 Logon time: 09-Dec-16 15:22:32 Logon server: DNS Domain: UPN: [5] Logon session 00000000:000003e5: User name: NT AUTHORITY\LOCAL SERVICE Auth package: Negotiate Logon type: Service Session: 0 Sid: S-1-5-19 Logon time: 09-Dec-16 15:22:32 Logon server: DNS Domain: UPN: ... [8] Logon session 00000000:0005c203: User name: NT VIRTUAL MACHINE\AC9081B6-1E96-4BC8-8B3B-C609D4F85F7D Auth package: Negotiate Logon type: Service Session: 0 → Anmeldung Kapitel 7 831
Sid: S-1-5-83-1-2895151542-1271406230-163986315-2103441620 Logon time: 09-Dec-16 15:22:35 Logon server: DNS Domain: UPN: [9] Logon session 00000000:0005d524: User name: NT VIRTUAL MACHINE\B37F4A3A-21EF-422D-8B37-AB6B0A016ED8 Auth package: Negotiate Logon type: Service Session: 0 Sid: S-1-5-83-1-3011463738-1110254063-1806382987-3631087882 Logon time: 09-Dec-16 15:22:35 Logon server: DNS Domain: ... [12] Logon session 00000000:0429ab2c: User name: IIS APPPOOL\DefaultAppPool Auth package: Negotiate Logon type: Service Session: 0 Sid: S-1-5-82-3006700770-424185619-1745488364-794895919-4004696415 Logon time: 09-Dec-16 22:33:03 Logon server: DNS Domain: UPN: Zu den Informationen, die über eine Sitzung angegeben werden, gehören die SID und der Name des betreffenden Benutzers, das Authentifizierungspaket und der Zeitpunkt der Anmeldung. Das Authentifizierungspaket Negotiate, das in den Anmeldesitzungen 2 und 9 der vorstehenden Ausgabe genannt wird, versucht je nachdem, was für die jeweilige Anforderung am geeignetsten ist, eine Authentifizierung über Kerberos oder über NTLM durchzuführen. Der LUID der Sitzung wird jeweils in der Zeile Logon session angezeigt. Mit dem Hilfsprogramm Handle.exe (ebenfalls von Sysinternals) können Sie die Tokens für eine gegebene Anmeldesitzung finden. Beispielsweise können Sie die Tokens für Sitzung 8 in der vorstehenden Ausgabe mit dem folgenden Befehl abrufen: C:\WINDOWS\system32>handle -a 5c203 Nthandle v4.1 - Handle viewer Copyright (C) 1997-2016 Mark Russinovich → 832 Kapitel 7 Sicherheit
Sysinternals - www.sysinternals.com System pid: 4 type: Directory 1274: \Sessions\0\ DosDevices\00000000-0005c203 lsass.exe pid: 496 type: Token D7C: NT VIRTUAL MACHINE\ AC9081B6-1E96-4BC8-8B3B-C609D4F85F7D:5c203 lsass.exe pid: 496 type: Token 2350: NT VIRTUAL MACHINE\ AC9081B6-1E96-4BC8-8B3B-C609D4F85F7D:5c203 lsass.exe pid: 496 type: Token 2390: NT VIRTUAL MACHINE\ AC9081B6-1E96-4BC8-8B3B-C609D4F85F7D:5c203 svchost.exe pid: 900 type: Token 804: NT VIRTUAL MACHINE\ AC9081B6-1E96-4BC8-8B3B-C609D4F85F7D:5c203 svchost.exe pid: 1468 type: Token 10EC: NT VIRTUAL MACHINE\ AC9081B6-1E96-4BC8-8B3B-C609D4F85F7D:5c203 vmms.exe pid: 4380 type: Token A34: NT VIRTUAL MACHINE\ AC9081B6-1E96-4BC8-8B3B-C609D4F85F7D:5c203 vmcompute.exe pid: 6592 type: Token 200: NT VIRTUAL MACHINE\ AC9081B6-1E96-4BC8-8B3B-C609D4F85F7D:5c203 vmwp.exe pid: 7136 type: WindowStation 168: \Windows\WindowStations\ Service-0x0-5c203$ vmwp.exe pid: 7136 type: WindowStation 170: \Windows\WindowStations\ Service-0x0-5c203$ Anschließend schaut sich Winlogon den Registrierungswert HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit an und erstellt einen Prozess, um das auszuführen, was dort angegeben ist. (Dieser Stringwert kann die Namen mehrerer EXE-Dateien, getrennt durch Kommata, enthalten.) Der Standardwert ist Userinit.exe, wodurch das Benutzerprofil geladen und ein Prozess erstellt wird, der die in HKCU\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ Winlogon\Shell genannte Datei ausführt. Falls es diesen Wert nicht gibt, schaut Userinit.exe in HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell nach, dessen Standardwert Explorer.exe lautet. Anschließend wird Userinit beendet. Das ist auch der Grund dafür, dass Sie keinen Elternprozess erkennen können, wenn Sie Explorer.exe in Process Explorer untersuchen. Weitere Informationen über die einzelnen Schritte des Anmeldeprozesses für Benutzer erhalten Sie in Kapitel 11 von Band 2. Sichere Authentifizierung Ein grundlegendes Problem bei der Authentifizierung anhand von Kennwörtern besteht darin, dass Kennwörter geknackt oder gestohlen und von Angreifern missbraucht werden können. Windows enthält einen Mechanismus, der erkennen kann, wie sicher die Form der Authentifizierung war, mit der sich ein Benutzer am System angemeldet hat. Dadurch kann der Zugriff auf bestimmte Objekte bei einer unsicheren Authentifizierung eingeschränkt werden. (Hierbei Anmeldung Kapitel 7 833
wird die Authentifizierung per Smartcard als sicherer eingestuft als die Verwendung von Kennwörtern.) Auf Systemen in einer Domäne kann der Domänenadministrator eine Zuordnung zwischen einer Objektkennung (einem eindeutigen numerischen String, der für einen bestimmten Objekttyp steht) in einem Zertifikat für die Authentifizierung eines Benutzers (z. B. einer Smartcard oder einem Hardware-Sicherheitstoken) und einer SID im Zugriffstoken des Benutzers aufstellen. Ein Zugriffssteuerungseintrag in der DACL eines Objekts kann dann eine solche SID als Teil eines Benutzertokens angeben, um Zugriff auf das Objekt zu erhalten. In technischem Sinne handelt es sich hierbei um einen Gruppenanspruch: Der Benutzer macht die Mitgliedschaft in einer bestimmten Gruppe geltend, der bestimmte Zugriffsrechte für bestimmte Objekte eingeräumt sind, wobei der Anspruch in diesem Fall auf dem verwendeten Authentifizierungsmechanismus beruht. Dieses Merkmal ist standardmäßig nicht aktiviert, sondern kann in Domänen mit zertifikatgestützter Authentifizierung vom Domänenadministrator eingerichtet werden. Die sichere Authentifizierung baut auf vorhandenen Windows-Sicherheitseinrichtungen auf und bietet IT-Administratoren sowie allen anderen, die sich um IT-Sicherheit einer Organisation kümmern, eine sehr große Flexibilität. Die Organisation entscheidet, welche Objektkennungen (Object Identifiers, OIDs) in die Zertifikate zur Benutzerauthentifizierung aufgenommen werden und wie die einzelnen OIDs den Active Directory-Universalgruppen (bzw. deren SIDs) zugeordnet werden. Anhand der Gruppenmitgliedschaft eines Benutzers kann erkannt werden, ob bei der Anmeldung ein Zertifikat verwendet wurde. Für die einzelnen Zertifikate können unterschiedliche Ausgaberichtlinien gelten, was wiederum unterschiedliche Grade an Sicherheit mit sich bringt, und dies kann berücksichtigt werden, um hochgradig sensible Objekte (wie Dateien oder auch alle anderen Objekte mit einem Sicherheitsdeskriptor) zu schützen. Authentifizierungsprotokolle rufen während der zertifikatgestützten Authentifizierung OIDs von Zertifikaten ab. Diese OIDs müssen SIDs zugeordnet werden, die wiederum bei der Erweiterung der Gruppenmitgliedschaften verarbeitet und in das Zugriffstoken aufgenommen werden. Die Zuordnung der OIDs zu den Universalgruppen wird in Active Directory festgelegt. Nehmen wir beispielsweise an, eine Organisation hat unterschiedliche Zertifikatausgaberichtlinien für Lieferanten, Vollzeitangestellte und das leitende Management, die den Universalgruppen Lieferanten-Benutzer, VZM-Benutzer bzw. LM-Benutzer zugeordnet sind. Die Benutzerin Abby hat eine Smartcard, deren Zertifikat mit der Richtlinie für das leitende Management ausgegeben wurde. Wenn sie sich mit ihrer Smartcard anmeldet, erhält sie die zusätzliche Mitgliedschaft in der Gruppe LM-Benutzer, was durch eine SID in ihrem Zugriffstoken angegeben wird. In den Zugriffssteuerungslisten von Objekten können Berechtigungen so angegeben werden, dass nur Mitglieder von VZM-Benutzer und LM-Benutzer – die anhand der entsprechenden SIDs in den Zugriffssteuerungseinträgen erkannt werden – Zugriff auf die Objekte erhalten. Bei der Anmeldung mit der Smartcard kann Abby auf diese Objekte zugreifen, doch wenn sie sich einfach nur mit ihrem Benutzernamen und Kennwort anmeldet, sind diese Objekte für sie nicht zugänglich, da ihr Zugriffstoken keine Mitgliedschaft in VZM-­ Benutzer oder LM-Benutzer angibt. Ein Benutzer mit einer Smartcard, die mithilfe der Richtlinie für Lieferanten ausgegeben wurde, kann überhaupt nicht auf Objekte zugreifen, deren Zugriffssteuerungseinträge die Mitgliedschaft in der Gruppe VZM-Benutzer oder LM-Benutzer erfordern. 834 Kapitel 7 Sicherheit
Das Windows-Biometrieframework Mit seinem Biometrieframework (WBF) stellt Windows einen Standardmechanismus zur Unterstützung bestimmter Arten von biometrischen Geräten bereit, z. B. von Fingerabdruckscannern. Wie bei vielen anderen Frameworks dieser Art wurde bei der Entwicklung von WBF darauf geachtet, die einzelnen Funktionen zur Unterstützung solcher Geräte zu isolieren und den erforderlichen Code zur Implementierung eines neuen Geräts zu minimieren. Abb. 7–21 zeigt die Hauptkomponenten von WBF. Sofern in der folgenden Aufstellung nicht anders vermerkt, werden alle diese Komponenten von Windows bereitgestellt: QQ Windows-Biometriedienst (%SystemRoot%\System32\Wbiosrvc.dll) Dieser Dienst stellt die Prozessausführungsumgebung bereit, in der ein oder mehrere Biometriedienstanbieter laufen können. QQ Windows Biometric Driver Interface (WBDI) Dies ist ein Satz von Schnittstellendefinitionen (IRP-Hauptfunktionscodes, DeviceIOControl-Codes usw.), denen Treiber für biometrische Geräte gehorchen müssen, um den Windows-Biometriedienst nutzen zu können. WBDI-Treiber können mit den normalen Treiberframeworks (UMDF, KMDF und WDM) entwickelt werden, wobei jedoch UMDF empfohlen wird, um den Codeumfang zu verringern und die Zuverlässigkeit zu erhöhen. WBDI wird in der Dokumentation des Windows Driver Kit beschrieben. QQ Windows-Biometrie-API Über diese API können Windows-Komponenten wie Winlog­ on und LogonUI auf den Biometriedienst zugreifen. Auch Drittanbieteranwendungen haben Zugang zu der Windows-Biometrie-API und können den Biometriescanner für andere Zwecke als die Anmeldung an Windows nutzen. Ein Beispiel für eine der Funktionen in dieser API ist WinBioEnumServiceProviders. Bereitgestellt wird die API in %SystemRoot%\ System32\Winbio.dll. QQ Fingerabdruck-Dienstanbieter Dieser Anbieter bildet einen Wrapper für die Funktionen eines Adapters für eine bestimmte Art von biometrischer Messung, um dem Windows-Biometriedienst eine allgemeine, von der Art der Messung unabhängige Schnittstelle zu präsentieren. In Zukunft können durch zusätzliche Biometrie-Dienstanbieter noch weitere Arten von biometrischen Messungen unterstützt werden, z. B. Retinascans und Stimmanalyse. Der biometrische Dienstanbieter wiederum nutzt die drei folgenden Benutzermodus-DLLs als Adapter: • Sensoradapter Stellt die Datenerfassungsfunktionen des Scanners bereit. Gewöhnlich greift der Sensoradapter mit Windows-E/A-Aufrufen auf die Scannerhardware zu. Windows enthält einen Sensoradapter, der für einfache Sensoren verwendet werden kann, für die bereits ein WBDI-Treiber vorhanden ist. Sensoradapter für kompliziertere Sensoren werden vom Sensorhersteller geschrieben. • Engine-Adapter Stellt u. a. die Verarbeitungs- und Vergleichsfunktionen für das Rohdatenformat des Scanners bereit. Dabei können die eigentlichen Verarbeitungs- und Vergleichsvorgänge in der DLL des Adapters erfolgen, es ist aber auch möglich, dass die DLL dazu mit einem anderen Modul kommuniziert. Der Engine-Adapter wird immer vom Sensorhersteller geliefert. Anmeldung Kapitel 7 835
• Speicheradapter Stellt Funktionen für eine sichere Speicherung bereit, um Vorlagen zu speichern und abzurufen, mit denen der Engine-Adapter die gescannten Biometriedaten vergleicht. Windows enthält einen Speicheradapter, der die Windows-Kryptografiedienste und eine reguläre Speicherung in Dateien auf der Festplatte nutzt. Sensorhersteller können eigene Speicheradapter anbieten. QQ Funktionaler Gerätetreiber für das biometrische Gerät Dieser Treiber macht die obere Seite des WBDI zugänglich. Gewöhnlich nutzt er für den Zugriff auf das biometrische Gerät die Dienste eines Bustreibers niedrigerer Ebene, z. B. des USB-Treibers. Dieser Treiber wird stets vom Sensorhersteller geliefert. Win32-Anwendung UWP-App Windows-Biometrie-API (WinBio.dll) Windows-Biometriedienst Biometrischer Dienstanbieter Sensoradapter Engine-Adapter Speicheradapter Windows Biometric Driver Interface Treiber Von Microsoft bereitgestellt Vom Gerätehersteller bereitgestellt Von Microsoft oder dem Gerätehersteller bereitgestellt Abbildung 7–21 Architektur und Komponenten des Windows-Biometrieframeworks Der typische Ablauf etwa bei der Anmeldung über einen Fingerabdruckscanner sieht so aus: 1. Nach der Initialisierung erhält der Sensoradapter vom Dienstanbieter eine Anforderung zur Datenerfassung. Daraufhin sendet der Sensoradapter eine DeviceIOControl-Anforderung mit dem Steuercode IOCTL_BIOMETRIC_CAPTURE_DATA an den WBDI-Treiber für den Fingerabdruckscanner. 2. Der WBDI-Treiber versetzt den Scanner in den Erfassungsmodus und stellt die IOCTL_­ BIOMETRIC_CAPTURE_DATA-Anforderung in die Warteschlange, bis ein Fingerabdruckscan erfolgt. 3. Der Benutzer hält seinen Finger auf den Scanner. Darüber erhält der WBDI-Treiber eine Benachrichtigung, sodass er die rohen Scandaten vom Sensor abruft und in einem Puffer, der mit der IOCTL_BIOMETRIC_CAPTURE_DATA-Anforderung verknüpft ist, an den Sensortreiber zurückgibt. 4. Der Sensoradapter präsentiert die Daten dem Dienstanbieter für Fingerabdrücke, der sie wiederum an den Engine-Adapter übergibt. 836 Kapitel 7 Sicherheit
5. Der Engine-Adapter verwandelt die Rohdaten in eine Form, die mit seinem Vorlagenspeicher kompatibel ist. 6. Mithilfe des Speicheradapters ruft der Dienstanbieter Vorlagen und zugehörige SIDs vom sicheren Speicher ab. Anschließend ruft er den Engine-Adapter auf, um die einzelnen Vorlagen mit den verarbeiteten Scandaten zu vergleichen. Der Engine-Adapter gibt einen Status zurück, der besagt, ob eine Übereinstimmung vorliegt oder nicht. 7. Bei einer Übereinstimmung benachrichtigt der Windows-Biometriedienst über eine Anmeldeinformationsanbieter-DLL Winlogon von der erfolgreichen Anmeldung und übergibt die SID des identifizierten Benutzers. Dies erfolgt über eine ALPC-Nachricht, sodass der Pfad nicht gefälscht werden kann. Windows Hello Das in Windows 10 eingeführte Windows Hello bietet neue Möglichkeiten zur biometrischen Identifizierung von Benutzern. Mit dieser Technologie können sich Benutzer einfach dadurch anmelden, dass sie in die Kamera des Geräts blicken oder ihren Finger auflegen. Zurzeit kann Windows Hello die folgenden drei Körpermerkmale zur biometrischen Identifizierung verwenden: QQ Fingerabdruck QQ Gesicht QQ Iris Betrachten wir hier zunächst die Sicherheitsaspekte. Wie wahrscheinlich ist es, dass eine andere Person für Sie gehalten wird? Wie wahrscheinlich ist es, dass Sie nicht als Sie selbst erkannt werden? Diese Fragen können wie folgt quantifiziert werden: QQ Falschakzeptanzrate (Einzigartigkeit) Das ist die Wahrscheinlichkeit dafür, dass die biometrischen Daten eines anderen Benutzers irrtümlich als die Ihren erkannt werden. Bei den von Microsoft verwendeten Algorithmen liegt diese Wahrscheinlichkeit bei 1:100.000. QQ Falschrückweisungsrate (Zuverlässigkeit) Das ist die Wahrscheinlichkeit dafür, dass Sie nicht als Sie selbst erkannt werden (z. B. bei ungünstiger Beleuchtung für die Gesichtsoder Iriserkennung). Bei der Implementierung von Microsoft beträgt diese Wahrscheinlichkeit weniger als 1 %. Sollte es geschehen, kann der Benutzer es noch einmal versuchen oder eine PIN eingeben. Eine PIN mag weniger sicher wirken als ein Kennwort (schließlich kann es sich bei der PIN auch einfach um eine vierstellige Zahl handeln). Allerdings ist eine PIN aus den beiden folgenden Gründen sicherer: QQ Die PIN wird nur lokal am Gerät eingegeben und nie über das Netzwerk übertragen. Wenn jemand die PIN in Erfahrung bringt, kann er sich damit also nicht von einem anderen Gerät aus anmelden. Kennwörter dagegen werden zum Domänencontroller übertragen. Wenn jemand ein Kennwort abfängt, kann er sich damit von einem anderen Rechner aus an der Domäne anmelden. Anmeldung Kapitel 7 837
QQ Die PIN wird im TPM gespeichert (Trusted Platform Module), einer Hardware, die auch beim sicheren Start eine Rolle spielt (siehe Kapitel 11 in Band 2), und ist daher nur sehr schwer zugänglich. Auf jeden Fall ist physischer Zugriff auf das Gerät erforderlich, was einen Angriff erheblich erschwert. Windows Hello baut auf dem im vorherigen Abschnitt beschriebenen Windows-Biometrie­ framework (WBF) auf. Auf modernen Laptops ist bereits eine Fingerabdruck- und Gesichtserkennung möglich, eine Iriserkennung dagegen gibt es zurzeit nur auf den Telefonen Microsoft Lumia 950 und 950 XL. (Allerdings ist damit zu rechnen, dass diese Funktion in Zukunft auch auf vielen anderen Geräten zur Verfügung stehen wird.) Beachten Sie, dass für die Gesichtserkennung sowohl eine Infrarot- als auch eine normale (RGB-) Kamera erforderlich ist. Unterstützt wird diese Funktion nur auf dem Microsoft Surface Pro 4 und dem Surface Book. Benutzerkontensteuerung und Virtualisierung Die Benutzerkontensteuerung (User Access Control, UAC) soll es ermöglichen, dass Benutzer ihre Arbeit mit Standardbenutzerrechten und nicht mit Administratorrechten ausführen. Ohne Administratorrechte können sie nicht versehentlich (oder absichtlich) Systemeinstellungen ändern und auch nicht auf sensible Informationen anderer Benutzer auf gemeinsam genutzten Computern zugreifen. Auch Malware ist unter diesen Umständen normalerweise nicht in der Lage, Sicherheitseinstellungen zu ändern oder Antivirussoftware auszuschalten. Die Arbeit unter Standardbenutzerrechten kann daher die Auswirkungen von Malware einschränken und sensible Daten schützen. Es mussten zunächst einige Probleme mit der Benutzerkontensteuerung gelöst werden, bevor sie für den praktischen Einsatz geeignet war. Erstens beruhte das bisherige Windows-Nutzungsmodell auf Administratorrechten, weshalb Softwareentwickler davon ausgingen, dass ihre Programme mit diesen Rechten ausgeführt wurden und daher auf beliebige Dateien, Registrierungsschlüssel und Betriebssystemeinstellungen zugreifen und diese ändern konnten. Zweitens brauchen Benutzer manchmal Administratorrechte, um beispielsweise Software zu installieren, die Systemzeit zu ändern und Ports in der Firewall zu öffnen. Bei eingeschalteter Benutzerkontensteuerung laufen die meisten Anwendungen mit Standardbenutzerrechten, auch wenn sich der Benutzer mit einem Konto angemeldet hat, das über Administratorrechte verfügt. Wenn es nötig ist, können Standardbenutzer jedoch auch Administratorrechte ausüben, beispielsweise für ältere Anwendungen, für die solche Rechte erforderlich sind, oder um Systemeinstellungen zu ändern. Dazu erstellt die Benutzerkontensteuerung bei der Anmeldung eines Benutzers mit einem Administratorkonto sowohl ein reguläres als auch ein gefiltertes Administratortoken. Bei allen Prozessen, die in der Sitzung dieses Benutzers erstellt werden, ist das gefilterte Token in Kraft, sodass Anwendungen, die mit Standardrechten laufen können, dies auch tun. Durch die Rechteerhöhung der Benutzerkontensteuerung kann ein als Administrator angemeldeter Benutzer jedoch auch Programme starten oder andere Funktionen ausführen, die volle Administratorrechte benötigen. 838 Kapitel 7 Sicherheit
Um die Nutzbarkeit der Standardbenutzerumgebung zu erhöhen, erlaubt Windows Standardbenutzern, auch einige Dinge zu tun, die früher Administratoren vorbehalten waren. Beispielsweise gibt es Gruppenrichtlinieneinstellungen, um Standardbenutzern die Installation von Druckern und Gerätetreibern zu erlauben, die von IT-Administratoren genehmigt wurden. Wenn Softwareentwickler ihre Tests in einer Umgebung mit Benutzerkontensteuerung ausführen, ermutigt sie das dazu, Anwendungen zu schreiben, die ohne Administratorrechte auskommen. Grundsätzlich sollten nicht administrative Programme keine Administratorrechte benötigen. Bei Programmen, die auf solche Rechte angewiesen sind, handelt es sich gewöhnlich um ältere Anwendungen, die veraltete APIs oder Techniken nutzen und aktualisiert werden sollten. Zusammengenommen haben all diese Änderungen dafür gesorgt, dass Benutzer nicht mehr ständig mit Administratorrechten arbeiten müssen. Virtualisierung des Dateisystems und der Registrierung Es gibt zwar durchaus Software, die tatsächlich Administratorrechte benötigt, doch viele Programme legen Benutzerdaten unnötigerweise in globalen Systemspeicherorten ab. Da eine Anwendung unter verschiedenen Benutzerkonten ausgeführt werden kann, sollte sie benutzerspezifische Daten und Einstellungen im %AppData%-Verzeichnis des Benutzers bzw. in seinem Registrierungsprofil unter HKEY_CURRENT_USER\Software speichern. Standardbenutzerkonten haben keinen Schreibzugriff auf das Verzeichnis %ProgramFiles% und auf HKEY_LOCAL_MACHINE\ Software, aber da die meisten Windows-Computer Einzelbenutzersysteme sind und die meisten Benutzer vor Einführung der Benutzerkontensteuerung Administratoren waren, funktionieren Anwendungen, die Benutzerdaten und -einstellungen dort ablegten, trotzdem. Durch die Virtualisierung des Dateisystem- und Registrierungsnamensraums macht Windows die Ausführung solcher veralteten Anwendungen unter Standardbenutzerkonten möglich. Wenn eine Anwendung einen globalen Systemspeicherort im Dateisystem oder in der Registrierung zu ändern versucht und diese Operation fehlschlägt, da der Zugriff verweigert wird, leitet Windows die Operation zu einem Bereich für den aktuellen Benutzer um. Liest eine Anwendung von einem globalen Systemspeicherort, schaut Windows erst im Benutzerbereich nach. Erst wenn dort keine Daten zu finden sind, erlaubt es der Anwendung, in dem globalen Speicherort zu lesen. Windows verwendet fast immer diese Form der Virtualisierung. Es gibt nur die folgenden Ausnahmefälle: QQ Es handelt sich um eine 64-Bit-Anwendung Da die Virtualisierung eine reine Kompatibilitätstechnologie ist, um auch ältere Anwendungen zu unterstützen, wird sie nur für 32-Bit-Anwendungen durchgeführt. Da 64-Bit-Anwendungen relativ neu sind, müssen ihre Entwickler die Richtlinien zum Schreiben von Anwendungen beachten, die unter Standardbenutzerrechten laufen können. QQ Die Anwendung wird bereits mit Administratorrechten ausgeführt In diesem Fall ist eine Virtualisierung nicht mehr notwendig. QQ Die Operation stammt von einem Aufrufer im Kernelmodus Benutzerkontensteuerung und Virtualisierung Kapitel 7 839
QQ Der Aufrufer hat während der Ausführung der Operation eine andere Identität angenommen Es werden nur Operationen virtualisiert, die von einem als veraltet (legacy) klassifizierten Prozess ausgehen. QQ Das ausführbare Abbild für den Prozess verfügt über ein UAC-kompatibles Manifest Das bedeutet, es enthält die Einstellung requestedExecutionLevel (siehe den nächsten Abschnitt). QQ Der Administrator hat keinen Schreibzugriff auf die Datei bzw. den Registrierungsschlüssel Die ältere Anwendung wäre auch fehlgeschlagen, wenn sie vor Einführung der Benutzerkontensteuerung mit Administratorrechten ausgeführt worden wäre. QQ Dienste werden niemals virtualisiert Den Virtualisierungsstatus eines Prozesses (der als Flag im Prozesstoken gespeichert wird) können Sie sich ansehen, indem Sie auf der Seite Details des Task-Managers die Spalte UAC-Virtualisierung hinzufügen (siehe Abb. 7–22). Bei den meisten Windows-Komponenten – zum Beispiel beim Desktopfenster-Manager (Dwm.exe), dem Client-Server-Laufzeit-Teilsystem (Scrss.exe) und dem Explorer – ist die Virtualisierung ausgeschaltet, da sie über ein UAC-kompatibles Manifest verfügen oder ohnehin mit Administratorrechten ausgeführt werden. Allerdings ist die Virtualisierung für die 32-Bit-Version von Internet Explorer (Iexplore.exe) eingeschaltet, da er ActiveX-Steuerelemente und Skripte ausführen kann, bei denen man davon ausgehen muss, dass sie mit Standardbenutzerrechten nicht richtig funktionieren. Bei Bedarf kann die Virtualisierung auf einem System mithilfe einer lokalen Sicherheitsrichtlinieneinstellung jedoch auch komplett abgeschaltet werden. Neben der Dateisystem- und Registrierungsvirtualisierung benötigen einige Anwendungen noch zusätzliche Hilfsmaßnahmen, um mit Standardbenutzerrechten laufen zu können. Stellen Sie sich beispielsweise eine Anwendung vor, die prüft, ob das Konto, unter dem sie ausgeführt wird, zur Administratorengruppe gehört. Ohne Mitgliedschaft in dieser Gruppe läuft diese Anwendung nicht, auch wenn sie sonst problemlos funktionieren würde. Damit solche Anwendungen trotzdem ausgeführt werden können, verfügt Windows über eine Reihe von Kompatibilitätsshims. Die am häufigsten verwendeten Shims, um älteren Anwendungen die Ausführung unter Standardbenutzerrechten zu ermöglichen, sind in Tabelle 7–15 aufgeführt. 840 Kapitel 7 Sicherheit
Abbildung 7–22 Anzeige des Virtualisierungsstatus im Task-Manager Benutzerkontensteuerung und Virtualisierung Kapitel 7 841
Flag Bedeutung ElevateCreateProcess Ändert die Funktion CreateProcess, sodass sie bei ERROR_ ELEVATION_REQUIRED-Fehlern den Anwendungsinformationsdienst aufruft, um eine Erhöhung der Rechte anzufordern. ForceAdminAccess Gibt die Mitgliedschaft in der Administratorengruppe vor. VirtualizeDeleteFile Gibt die erfolgreiche Löschung von globalen Dateien und Verzeichnissen vor. LocalMappedObject Erzwingt die Aufnahme von globalen Abschnittsobjekten in den Namensraum des Benutzers. VirtualizeHKCRLite Leitet die globale Registrierung von COM-Objekten in einen Speicherort für den Benutzer um. VirtualizeRegisterTypeLib Wandelt computerbezogene typelib-Registrierungen in benutzerbezogene Registrierungen um. Tabelle 7–15 Kompatibilitätsshims Dateivirtualisierung Bei den Dateisystemspeicherorten, die für veraltete Prozesse virtualisiert werden, handelt es sich um %ProgramFiles%, %ProgramData% und %SystemRoot%, wobei jedoch einige Unterverzeichnisse sowie jegliche Arten von ausführbaren Dateien (.exe, .bat, .scr, .vbs usw.) ausgenommen sind. Wenn ein Programm versucht, sich unter einem Standardbenutzerkonto selbst zu aktualisieren, schlägt es daher fehl, anstatt private Versionen der ausführbaren Dateien anzulegen, die für Administratoren mit einem globalen Updater nicht sichtbar wären. Um Dateierweiterungen zu der Ausnahmeliste hinzuzufügen, geben Sie sie im Registrierungsschlüssel HKLM\System\CurrentControlSet\Services\Luafv\Parameters\ ExcludedExtensionsAdd ein und starten den Rechner neu. Verwenden Sie einen Multistringtyp, um mehrere Erweiterungen anzugeben, und lassen Sie den führenden Punkt bei der Erweiterung weg. Änderungen, die veraltete Prozesse an virtualisierten Verzeichnissen vornehmen wollen, werden an das virtuelle Stammverzeichnis des Benutzers weitergeleitet, also an %LocalAppData%\ VirtualStore. Die Bezeichnung Local in diesem Pfad deutet darauf hin, dass virtualisierte Dateien nicht zusammen mit dem Rest eines servergespeicherten Profils umziehen. Der Filtertreiber UAC-Dateivirtualisierung (UAC File Virtualization, %SystemRoot%\­ System32\Drivers\Luafv.sys) implementiert die Dateisystemvirtualisierung. Da es sich um einen Dateisystem-Filtertreiber handelt, sieht er alle Operationen im lokalen Dateisystem, allerdings wirkt er sich nur auf Operationen aus, die von veralteten Prozessen ausgehen. Wie Abb. 7–23 zeigt, ändert der Filtertreiber den Zieldateipfad für veraltete Prozesse, die eine Datei an einem globalen Systemspeicherort erstellen wollen, aber nicht für nicht virtualisierte Prozesse mit Standardbenutzerrechten. Die Standardberechtigungen für das Verzeichnis 842 Kapitel 7 Sicherheit
\Windows verweigern den Zugriff für Anwendungen, die mit UAC-Unterstützung geschrieben wurden. Ältere Prozesse aber verhalten sich so, als hätte die Operation tatsächlich Erfolg, obwohl die Datei in Wirklichkeit an einem Speicherort erstellt wird, der für den Benutzer voll zugänglich ist. Veraltete Anwendung UAC-fähige Anwendung Schreibt in \Windows\App.ini Benutzermodus Kernelmodus Schreibt in \Users\<Benutzer>\ AppData\Local\ VirtualStore\Windows\ App.ini Schreibt in \Windows\App.ini ZUGRIFF VERWEIGERT Abbildung 7–23 Funktionsweise des Filtertreibers für die UAC-Dateivirtualisierung Experiment: Dateivirtualisierung In diesem Experiment aktivieren und deaktivieren Sie die Virtualisierung an der Befehlszeile und beobachten die Auswirkungen: 1. Öffnen Sie eine Eingabeaufforderung ohne erhöhte Rechte (die Benutzerkontensteuerung muss dazu eingeschaltet sein) und aktivieren Sie die Virtualisierung dafür, indem Sie auf der Registerkarte Details des Task-Managers auf den Prozess rechtsklicken und UAC-Virtualisierung auswählen. 2. Begeben Sie sich zum Verzeichnis C:\Windows und schreiben Sie mit dem folgenden Befehl eine Datei: echo hello-1 > test.txt 3. Listen Sie den Verzeichnisinhalt auf. Die Datei wird angezeigt. dir test.txt 4. Deaktivieren Sie die Virtualisierung, indem Sie auf der Registerkarte Details des Task-Managers auf den Prozess rechtsklicken und UAC-Virtualisierung abwählen. Listen Sie erneut wie in Schritt 3 den Verzeichnisinhalt auf. Die Datei ist verschwunden! Wenn Sie dagegen das Verzeichnis VirtualStore auflisten, wird sie wieder angezeigt: dir %LOCALAPPDATA%\VirtualStore\Windows\test.txt → Benutzerkontensteuerung und Virtualisierung Kapitel 7 843
5. Aktivieren Sie die Virtualisierung für den Prozess wieder. 6. Um sich einen anspruchsvolleren Fall anzusehen, öffnen Sie eine weitere Eingabeaufforderung, diesmal aber mit erhöhten Rechten. Wiederholen Sie Schritt 2 und 3 mit dem String hello-2. 7. Untersuchen Sie an beiden Eingabeaufforderungen den Text innerhalb der Dateien mit dem folgenden Befehl. Die folgenden Screenshots zeigen die zu erwartende Ausgabe. type test.txt → 844 Kapitel 7 Sicherheit
8. Löschen Sie die Datei test.txt an der Eingabeaufforderung mit erhöhten Rechten: del test.txt 9. Wiederholen Sie Schritt 3 in beiden Fenstern. In der Eingabeaufforderung mit erhöhten Rechten wird die Datei nicht mehr gefunden, während die Standardeingabeaufforderung wieder den alten Inhalt der Datei anzeigt. Das macht den zuvor beschriebenen Failovermechanismus deutlich: Leseoperationen suchen erst im virtuellen Speicher des Benutzers, doch wenn die gewünschte Datei dort nicht zu finden ist, erfolgt der Zugriff auf den Systemspeicherort. Virtualisierung der Registrierung Die Virtualisierung der Registrierung unterscheidet sich leicht von der Virtualisierung des Dateisystems. Zu den virtualisierten Schlüsseln gehören die meisten unter HKEY_LOCAL_MACHINE\ Software, wobei es aber auch zahlreiche Ausnahmen gibt, darunter die folgenden: QQ HKLM\Software\Microsoft\Windows QQ HKLM\Software\Microsoft\Windows NT QQ HKLM\Software\Classes Nur Schlüssel, die üblicherweise von veralteten Anwendungen bearbeitet werden und die nicht zu Kompatibilitäts- oder Interoperabilitätsproblemen führen, werden virtualisiert. Windows leitet Änderungen, die veraltete Anwendungen an virtualisierten Schlüsseln vornehmen, zum virtuellen Registrierungsstamm für den Benutzer unter HKEY_CURRENT_USER\Software\Classes\ VirtualStore um. Der Schlüssel befindet sich im Hive Classes (%LocalAppData%\Microsoft\ Windows\UsrClass.dat), der wie alle anderen Inhalte virtualisierter Dateien nicht zusammen mit einem servergespeicherten Benutzerprofil umzieht. Windows unterhält jedoch keine feste Liste virtualisierter Speicherorte, wie es beim Dateisystem der Fall ist, sondern speichert den Virtualisierungsstatus eines Schlüssel in Form einer Kombination von Flags (siehe Tabelle 7–16). Sie können beim Windows-Dienstprogramm Reg.exe die Option flags einsetzen, um den Virtualisierungsstatus eines Schlüssel abzurufen oder festzulegen. In Abb. 7–24 ist der Schlüssel HKLM\Software vollständig virtualisiert, doch für den Unterschlüssel Windows (und alle seine Kindschlüssel) ist nur der »Fehlschlag ohne Rückmeldung« aktiviert. Benutzerkontensteuerung und Virtualisierung Kapitel 7 845
Flag Bedeutung REG_KEY_DONT_VIRTUALIZE Gibt an, ob die Virtualisierung für den Schlüssel aktiviert ist. Ist das Flag gesetzt, so ist die Virtualisierung deaktiviert. REG_KEY_DONT_SILENT_FAIL Wenn das Flag REG_KEY_DONT_VIRTUALIZE gesetzt (die Virtualisierung also deaktiviert) ist, gibt dieses Flag an, dass eine veraltete Anwendung, der normalerweise der Zugriff für eine Operation an dem Schlüssel verweigert würde, nicht die angeforderten Rechte, sondern die maximal zulässigen Rechte (MAXIMUM_ALLOWED) für den Schlüssel erhält (wodurch jeglicher Zugriff gewährt wird, der dem Konto erlaubt ist). Wenn dieses Flag gesetzt ist, wird damit implizit auch die Virtualisierung ausgeschaltet. REG_KEY_RECURSE_FLAG Gibt an, ob die Virtualisierungsflags an die Unterschlüssel des vorliegenden Schlüssels vererbt werden. Tabelle 7–16 Virtualisierungsflags für die Registrierung Abbildung 7–24 Flags für die Virtualisierung der Registrierungsschlüssel Software und Windows Im Gegensatz zur Dateivirtualisierung, für die ein Filtertreiber verwendet wird, ist die Virtualisierung der Registrierung im Konfigurations-Manager implementiert (siehe Kapitel 9 in Band 2). Die Funktionsweise ähnelt aber der Dateivirtualisierung: Auch hier wird ein veralteter Prozess, der einen Unterschlüssel eines virtuellen Schlüssels erstellt, zum virtuellen Registrierungsstamm des Benutzers umgeleitet, während UAC-kompatiblen Prozessen aufgrund der Standardberechtigungen der Zugriff verweigert wird (siehe Abb. 7–25). 846 Kapitel 7 Sicherheit
Virtualisierter Prozess Nicht virtualisierter Prozess Schreibt in HKLM\Software\App ZUGRIFF VERWEIGERT Benutzermodus Kernelmodus Schreibt in HKCU\Software\Classes\ VirtualStore\Machine\ Software\App Registrierung Abbildung 7–25 Funktionsweise der Registrierungsvirtualisierung Rechteerhöhung Auch wenn Benutzer nur Programme ausführen, die unter Standardbenutzerrechten funktionieren, benötigen sie für einige Operationen trotzdem Administratorrechte, etwa für Installation von Software, bei der Verzeichnisse und Registrierungsschlüssel in globalen Systemspeicherorten gespeichert oder Dienste und Gerätetreiber eingerichtet werden müssen. Auch für die Bearbeitung von systemweiten Windows- und Anwendungseinstellungen und die Jugendschutzfunktionen sind Administratorrechte erforderlich. Es wäre bei den meisten dieser Vorgänge zwar möglich, zu einem dedizierten Administratorkonto umzuschalten, aber das wäre so unbequem, dass es viele Benutzer dazu veranlassen könnte, ihre gesamten täglichen Arbeiten unter ihrem Administratorkonto auszuführen, auch wenn für die meisten gar keine Administratorrechte erforderlich sind. Die Rechteerhöhung durch die Benutzerkontensteuerung ist jedoch nur eine Maßnahme zur Steigerung der Benutzerfreundlichkeit. Eine Sicherheitsgrenze wird dadurch nicht definiert, da es keine Richtlinie gibt, die bestimmt, wer oder was die Grenze überschreiten darf und wer oder was nicht. Benutzerkonten sind Sicherheitsgrenzen, da ein Benutzer nicht auf die Daten eines anderen zugreifen kann, ohne über dessen Berechtigungen zu verfügen. Es ist daher nicht garantiert, dass Malware, die unter Standardbenutzerrechten läuft, nicht in der Lage ist, einen Prozess mit erhöhten Rechten zu missbrauchen, um Administratorrechte zu gewinnen. In Dialogfeldern zur Rechteerhöhung wird die ausführbare Datei angegeben, deren Rechte erhöht werden, aber es wird nicht gesagt, was sie damit anstellt. Benutzerkontensteuerung und Virtualisierung Kapitel 7 847
Ausführung mit Administratorrechten Windows schließt eine erweiterte »Ausführen als«-Funktion ein, mit der Standardbenutzer auf einfache Weise Prozesse mit Administratorrechten starten können. Dazu müssen Anwendungen in der Lage sein, die Operationen zu erkennen, für die das System ihnen Administratorrechte einräumen kann (mehr darüber in Kürze). Um Benutzer, die Systemadministratoren sind, die Arbeit mit Standardbenutzerrechten zu ermöglichen, ohne dass sie bei jedem Zugriff auf Administratorrechte Benutzername und Kennwort eingeben müssen, verwendet Windows den sogenannten Administratorgenehmigungsmodus (Admin Approval Mode, AAM). Dieser Mechanismus erstellt bei der Anmeldung zwei Identitäten für den Benutzer, nämlich eine mit Standardbenutzer- und eine mit Adminis­ tratorrechten. Da jeder Benutzer auf einem Windows-System entweder ein Standardbenutzer ist oder größtenteils als Standardbenutzer im AAM arbeitet, können Entwickler davon aus­ gehen, dass alle Windows-Benutzer Standardbenutzer sind. Das führt dazu, dass mehr Programme geschrieben werden, die auch ohne Virtualisierung und Shims mit Standardbenutzerrechten funktionieren. Einem Prozess Administratorrechte zu gewähren, wird als Rechteerhöhung oder Rechte­ erweiterung bezeichnet. Erfolgt dieser Vorgang für ein Standardbenutzerkonto (oder einen Benutzer, der zwar zu einer administrativen Gruppe, aber nicht zur Gruppe Administratoren gehört), so spricht man von einer Über-die-Schulter-Erweiterung (Over the Shoulder, OTS), da dabei die Anmeldeinformationen eines Mitglieds der Gruppe Administratoren bereitgestellt werden müssen, was gewöhnlich dadurch geschieht, dass ein entsprechend privilegierter Benutzer seine Anmeldeinformationen sozusagen »über die Schulter« des Standardbenutzers eingibt. Die Rechteerhöhung für einen AAM-Benutzer dagegen wird als Erweiterung mit Zustimmung (Consent Elevation) bezeichnet, da der Benutzer einfach der Zuweisung von Administratorrechten zustimmen muss. An Domänen angeschlossene Systeme handhaben den AAM-Zugriff von Remotebenutzern auf andere Weise als eigenständige Systeme (also gewöhnliche Privatcomputer), da sie in den Berechtigungen für Ressourcen auch administrative Domänengruppen verwenden können. Wenn ein Remotebenutzer auf eine Dateifreigabe auf einem eigenständigen Computer zugreift, fordert Windows seine Standardbenutzeridentität an. Auf Systemen in Domänen dagegen fordert Windows die administrative Identität des Benutzers an und kann damit seine Mitgliedschaften in allen Domänengruppen berücksichtigen. Wird ein Abbild ausgeführt, das Administratorrechte anfordert, startet der Anwendungsinformationsdienst (AIS, enthalten in %SystemRoot%\System32\Appinfo.dll), der innerhalb eines regulären Diensthostprozesses (SvcHost.exe) läuft, %SystemRoot%\System32\Consent.exe. Dieses Programm fertigt ein Bitmapbild des Bildschirms an, wendet einen Verdunkelungseffekt darauf an, schaltet zum sicheren Desktop um, der nur für das lokale Systemkonto zugänglich ist, verwendet das Bitmapbild als Hintergrund dafür und zeigt das Dialogfeld zur Rechteerhöhung mit Angaben zu der anfordernden Anwendung an. Durch die Anzeige dieses Dialogfelds in einem separaten Desktop wird verhindert, dass irgendeine Anwendung, die unter dem Benutzerkonto ausgeführt wird, das Erscheinungsbild dieses Dialogfelds manipuliert. Handelt es sich bei dem Abbild um eine (von Microsoft oder einer anderen Stelle) signierte Windows-Komponente, so wird das Dialogfeld wie in dem linken Dialogfeld von Abb. 7–26 mit 848 Kapitel 7 Sicherheit
einem blauen Streifen am oberen Rand dargestellt. (Die Unterscheidung zwischen Signaturen von Microsoft und von anderen Stellen wurde mit Windows 10 aufgehoben.) Ist das Abbild nicht signiert, so wird der Streifen in Gelb angezeigt und darauf hingewiesen, dass der Ursprung des Abbilds unbekannt ist (siehe rechte Seite von Abb. 7–26). Das Dialogfeld zur Rechteerhöhung zeigt bei digital signierten Abbildern das Programmsymbol, eine Beschreibung sowie den Herausgeber an und bei nicht signierten nur den Dateinamen und Herausgeber: Unbekannt. Diese Unterschiede erschweren es Malware, das Erscheinungsbild legitimer Software nachzuahmen. Über den Link Details anzeigen unten in dem Dialogfeld können Sie die Befehlszeile einsehen, die beim Start der ausführbaren Datei übergeben wird. Abbildung 7–26 Dialogfelder zur AAM-Rechteerhöhung für Anwendungen mit und ohne Signatur Das Dialogfeld für die OTS-Rechteerweiterung aus Abb. 7–27 sieht ähnlich aus, fordert aber zur Eingabe der Anmeldeinformationen eines Administrators auf und listet alle Konten mit Administratorrechten auf. Abbildung 7–27 Dialogfeld zur OTS-Rechteerhöhung Wenn ein Benutzer die angeforderte Rechteerhöhung ablehnt, gibt Windows einen Fehler aufgrund einer Zugriffsverweigerung an den Prozess zurück, der den Vorgang ausgelöst hat. Stimmt der Benutzer dagegen zu, indem er die Anmeldeinformationen eines Administrators Benutzerkontensteuerung und Virtualisierung Kapitel 7 849
eingibt oder auf Ja klickt, ruft der Anmeldeinformationsdienst CreateProcessAsUser auf, um den Prozess mit der entsprechenden Administratoridentität zu starten. Der Anmeldeinformationsdienst ist zwar technisch gesehen der Elternprozess des Prozesses mit erhöhten Rechten, verwendet aber die neuen Möglichkeiten der API CreateProcessAsUser, um die Elternprozesskennung auf diejenige des Prozesses zu setzen, der den Dienst ursprünglich gestartet hat. Aus diesem Grunde erscheinen Prozesse mit erhöhten Rechten in den Prozessbäumen von Programmen wie Process Explorer nicht als Kinder des Hostprozesses, in dem der Anmeldeinformationsdienst ausgeführt wird. Abb. 7–28 zeigt die Vorgänge, die ablaufen, wenn von einem Standardbenutzerkonto aus ein Prozess mit erhöhten Rechten gestartet wird. Neuzuweisung der Elternrolle Explorer (Standardbenutzer) Anwendungs­ informations­dienst App.Exe Consent.exe (Lokales System) (Administrator) Abbildung 7–28 Starten einer administrativen Anwendung als Standardbenutzer Administratorrechte anfordern Die Notwendigkeit von Administratorrechten wird vom System und von Anwendungen auf verschiedene Weisen angezeigt. So kann beispielsweise im Kontextmenü und in den Optionen einer Verknüpfung der Befehl Als Administrator ausführen angezeigt werden. Dabei wird auch jeweils das Symbol des blau-goldenen Schildes dargestellt, mit dem alle Schaltflächen und Menüeinträge gekennzeichnet werden, deren Auswahl eine Rechteerhöhung zur Folge hat. Wird Als Administrator ausführen ausgewählt, ruft der Explorer die API ShellExecute mit dem Verb runas aus. Da fast alle Installationsprogramme Administratorrechte erfordern, enthält der Abbildlader, der den Start einer ausführbaren Datei auslöst, Code zur Erkennung möglicherweise veralteter Installer. Dabei werden einerseits sehr einfache Heuristiken eingesetzt, die die internen Versionsinformationen abfragen oder prüfen, ob im Dateinamen des Abbilds Wörter wie setup, install oder update vorkommen, aber auch anspruchsvollere Erkennungsmethoden, bei denen in der ausführbaren Datei nach Bytesequenzen gesucht wird, die für Installationswrapper von Drittanbietern typisch sind. Des Weiteren ruft der Abbildlader die Anwendungskompatibilitätsbibliothek auf, um zu sehen, ob die betreffende ausführbare Datei Administratorrechte 850 Kapitel 7 Sicherheit
benötigt. Die Bibliothek wiederum schaut in der Anwendungskompatibilitätsdatenbank nach, ob für die ausführbare Datei das Kompatibilitätsflag RequireAdministrator oder RunAsInvoker angegeben ist. Die üblichste Vorgehensweise, mit der ausführbare Dateien Administratorrechte anfordern, besteht darin, das Tag requestedExecutionLevel in ihr Manifest aufzunehmen. Das Attribut für die Ausführungsebene in diesem Element kann die drei Werte aus Tabelle 7–17 annehmen. Ausführungsebene Bedeutung Verwendung Als aufrufende Instanz Keine Administratorrechte erforderlich; es wird niemals eine Rechteerhöhung angefordert. Gewöhnliche Benutzeranwendungen, die keine Administratorrechte benötigen, z. B. der Editor Die am höchsten verfügbare [sic!] Genehmigung für die höchsten verfügbaren Rechte wird angefordert. Wenn der Benutzer als Standardbenutzer angemeldet ist, wird der Prozess als aufrufende Instanz gestartet. Anderenfalls erscheint ein AAM-Dialogfeld zur Rechteerhöhung, sodass der Prozess mit vollen Administratorrechten laufen kann. Anwendungen, die ohne volle Administratorrechte funktionieren, bei denen aber zu erwarten ist, dass die Benutzer einen Administratorzugriff anfordern, wenn er leicht erhältlich ist. Diese Ausführungsebene wird beispielsweise vom Registrierungseditor, der Microsoft Management Console und der Ereignisanzeige genutzt. Administrator erforderlich Es werden stets Administratorrechte angefordert. Für Standardbenutzer wird das OTS-Dialogfeld zur Rechteerhöhung angezeigt, anderenfalls das AAM-Dialogfeld. Anwendungen, die Administra­ torrechte benötigen, um zu funktionieren, z. B. der Editor für Firewalleinstellungen, der sich auf die systemweite Sicherheit auswirkt. Tabelle 7–17 Angeforderte Ausführungsebenen Ist in einem Manifest das Element trustInfo vorhanden (wie in der folgenden Ausgabe des Manifests von Eventvwr.exe), wurde die ausführbare Datei mit UAC-Unterstützung geschrieben. In diesem Element ist außerdem das Element requestedExecutionLevel verschachtelt. Im Attribut uiAccess setzen die Programme für Eingabehilfen die zuvor erwähnten UIPI-Umgehungsfunktionen ein. C:\>sigcheck -m c:\Windows\System32\eventvwr.exe ... <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <security> <requestedPrivileges> <requestedExecutionLevel level="highestAvailable" uiAccess="false" /> </requestedPrivileges> </security> </trustInfo> Benutzerkontensteuerung und Virtualisierung Kapitel 7 851
<asmv3:application> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/ WindowsSettings"> <autoElevate>true</autoElevate> </asmv3:windowsSettings> </asmv3:application> ... Automatische Rechteerweiterung In der Standardkonfiguration (wie Sie sie ändern können, erfahren Sie im nächsten Abschnitt) wird bei den meisten ausführbaren Windows-Dateien und Systemsteuerungsapplets kein Dialogfeld zur Rechteerhöhung angezeigt, selbst wenn sie Administratorrechte benötigen. Das liegt an der automatischen Rechteerweiterung, die entwickelt wurde, damit Administratoren bei ihrer Arbeit nicht ständig mit Dialogfeldern zur Rechteerhöhung konfrontiert werden. Die entsprechenden Programme werden automatisch mit dem vollständigen Administratortoken des Benutzers ausgeführt. Für die automatische Rechteerweiterung gelten mehrere Voraussetzungen. Erstens muss die fragliche ausführbare Datei eine Windows-Datei sein – das heißt, sie muss vom Herausgeber Windows signiert sein. (Also nicht einfach nur von Microsoft. Es mag merkwürdig erscheinen, aber diese beiden Herausgeber sind nicht identisch. Anwendungen mit Windows-Signatur gelten als privilegierter als solche mit Microsoft-Signatur.) Außerdem muss sie sich in einem der als sicher eingestuften Verzeichnisse befinden: %SystemRoot%\System32 und die meisten seiner Unterverzeichnisse, %Systemroot%\Ehome sowie einige wenige Verzeichnisse unter %ProgramFiles% (etwa diejenigen mit Windows Defender und Windows Journal). Je nach Art der ausführbaren Datei kann es noch weitere Voraussetzungen geben. EXE-Dateien außer Mmc.exe erhalten eine automatische Rechteerweiterung, wenn dies mit dem Element autoElevate in ihrem Manifest angefordert wird. Das können Sie auch in dem Manifest von Eventvwr.exe aus dem vorherigen Abschnitt sehen. Mmc.exe ist ein Sonderfall. Ob eine automatische Rechteerweiterung erfolgt oder nicht, hängt davon ab, welches Snap-In es lädt. Gewöhnlich wird Mmc.exe an der Befehlszeile unter Angabe einer MSC-Datei aufgerufen, die das zu ladende Snap-In angibt. Wird Mmc.exe unter einem geschützten Administratorkonto ausgeführt (also einem Konto mit eingeschränktem Administratortoken), fordert sie Administratorrechte an. Windows vergewissert sich, dass es sich bei Mmc.exe um eine ausführbare Windows-Datei handelt, und schaut sich dann die MSC-Datei an. Auch bei ihr muss es sich um eine ausführbare Windows-Datei handeln, und darüber hinaus muss sie auf der internen Liste von MSCs mit automatischer Rechteerweiterung stehen (was bei fast allen MSC-Dateien in Windows der Fall ist). COM-Klassen (Out-of-Process-Server) fordern Administratorrechte mithilfe ihrer Registrierungsschlüssel an. Dazu ist der Unterschlüssel Elevation mit dem DWORD-Wert Enabled erforderlich, der auf 1 gesetzt sein muss. Sowohl die COM-Klasse als auch die ausführbare Datei, die sie instanziiert, müssen die Anforderungen für ausführbare Windows-Dateien erfüllen. Es ist jedoch nicht nötig, dass die ausführbare Datei selbst eine automatische Rechteerweiterung anfordert. 852 Kapitel 7 Sicherheit
Das Verhalten der Benutzerkontensteuerung festlegen Das Verhalten der Benutzerkontensteuerung lässt sich mit dem Dialogfeld aus Abb. 7–29 ändern, das unter Einstellungen der Benutzerkontensteuerung ändern zur Verfügung steht. In der Abbildung sehen Sie die Standardeinstellungen. Abbildung 7–29 Einstellungen zur Benutzerkontensteuerung Die Bedeutungen der vier möglichen Einstellungen werden in Tabelle 7–18 beschrieben. Von der dritten Einstellung ist abzuraten, da das Dialogfeld zur Rechteerhöhung dabei auf dem normalen Desktop des Benutzers und nicht auf dem sicheren Desktop erscheint, sodass Schadprogramme, die in derselben Sitzung ausgeführt werden, sein Aussehen ändern könnten. Diese Einstellung ist nur für Systeme gedacht, bei denen das Videoteilsystem sehr lange braucht, um den Desktop abzudunkeln, oder auf denen die übliche Darstellung der Benutzerkontensteuerung aus anderen Gründen nicht geeignet ist. Benutzerkontensteuerung und Virtualisierung Kapitel 7 853
Reglerstellung Ein Administrator, dessen Konto zurzeit nicht mit Administratorrechten ausgeführt wird, versucht ... Bemerkungen ... Windows-Einstellungen zu ändern (z. B. bestimmte Systemsteuerungs-Applets) ... Software zu installieren, ein Programm zu starten, dessen Manifest eine Rechteerhöhung verlangt, oder ein Programm mit »Als Administrator ausführen« zu starten Höchste Stellung (Immer benachrichtigen) Das Dialogfeld der Benutzerkontensteuerung zur Rechte­ erhöhung wird auf dem sicheren Desktop angezeigt. Das Dialogfeld der Benutzerkontensteuerung zur Rechte­ erhöhung wird auf dem sicheren Desktop angezeigt. Dies war das Verhalten von Windows Vista. Zweite Stellung Die Rechte werden automatisch erhöht, ohne dass ein Dialogfeld oder eine Benachrichtigung angezeigt wird. Das Dialogfeld der Benutzerkontensteuerung zur Rechteerhöhung wird auf dem sicheren Desktop angezeigt. Standardeinstellung von Windows Dritte Stellung Die Rechte werden automatisch erhöht, ohne dass ein Dialogfeld oder eine Benachrichtigung angezeigt wird. Das Dialogfeld der Benutzerkontensteuerung zur Rechteerhöhung wird auf dem normalen Desktop des Benutzers angezeigt. Nicht empfehlenswert Unterste Stellung (Nie benachrichtigen) Die Benutzerkontensteuerung ist für Administratoren ausgeschaltet. Die Benutzerkontensteuerung ist für Administratoren ausgeschaltet. Nicht empfehlenswert Tabelle 7–18 Einstellungen für die Benutzerkontensteuerung Von der untersten Stellung wird dringend abgeraten, da die Benutzerkontensteuerung dabei für Administratorkonten ganz abgeschaltet ist. Alle Prozesse, die ein Benutzer mit einem Adminis­ tratorkonto ausführt, laufen mit den vollen Administratorrechten und nicht mit einem gefilterten Administratortoken. Auch die Virtualisierung der Registrierung und des Dateisystems und der geschützte Modus von Internet Explorer sind für die betreffenden Konten ausgeschaltet. Für nicht administrative Konten dagegen ist die Virtualisierung nach wie vor in Kraft, und bei ihnen wird auch ein OTS-Dialogfeld angezeigt, wenn die Benutzer versuchen, Windows-Einstellungen zu ändern, ein Programm zu starten, das eine Rechteerweiterung benötigt, oder die Option Als Administrator ausführen zu nutzen. Die Einstellungen für die Benutzerkontensteuerung werden in vier Werten des Registrierungsschlüssels HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System gespeichert (siehe Tabelle 7–19). ConsentPromptBehaviorAdmin steuert das Dialogfeld zur Rechteerhöhung für Administratoren mit gefiltertem Administratortoken, ConsentPromptBehaviorUser das entsprechende Dialogfeld für Benutzer, die keine Administratoren sind. Reglerstellung Höchste Stellung (Immer benachrichtigen) 854 Kapitel 7 ConsentPrompt​ BehaviorAdmin ConsentPrompt​ BehaviorUser 2 (zeigt das AAM-Dialogfeld zur Rechteerweiterung an) 3 (zeigt das OTS-Dialogfeld zur Rechteerweiterung an) Sicherheit EnableLua PromptOnSecure​ Desktop 1 (aktiviert) 1 (aktiviert) →
ConsentPrompt​ BehaviorAdmin ConsentPrompt​ BehaviorUser EnableLua PromptOnSecure​ Desktop Zweite Stellung 5 (zeigt das AAM-Dialogfeld zur Rechteerweiterung an, außer bei Änderungen an Windows-Einstellungen) 3 1 1 Dritte Stellung 5 3 1 0 (deaktiviert; das Dialogfeld der Benutzerkontensteuerung erscheint auf dem normalen Desktop des Benutzers) Unterste Stellung (Nie benachrichtigen) 0 3 0 (deaktiviert; bei Anmeldungen an Administratorkonten wird kein eingeschränktes Administratortoken erstellt) 0 Reglerstellung Tabelle 7–19 Registrierungswerte für die Benutzerkontensteuerung Schutz gegen Exploits In diesem Kapitel haben Sie eine Reihe von Technologien gesehen, um die Benutzer zu schützen, das Signieren von ausführbarem Code durchzusetzen und den Zugriff auf Ressourcen durch Sandboxes einzuschränken. Letzten Endes aber weisen alle Sicherheitssysteme Schwachstellen und alle Programme Bugs auf, während Angreifer immer anspruchsvollere Methoden einsetzen, um sie auszunutzen. Ein Sicherheitsmodell, bei dem vorausgesetzt wird, dass der Code vollständig fehlerfrei ist oder dass sämtliche Bugs gefunden und ausgemerzt werden können, ist zum Scheitern verurteilt. Außerdem bieten viele Sicherheitsfunktionen »Garantien« zur Codeausführung nur auf Kosten der Leistung oder Kompatibilität, was in manchen Situa­ tionen nicht akzeptabel ist. Ein erfolgversprechenderer Ansatz besteht darin, die gängigsten Techniken von Angreifern zu ermitteln sowie mithilfe eines eigenen »Red Teams« (das die eigene Software anzugreifen versucht) neue Techniken aufzuspüren, bevor Angreifer darauf kommen, und Abwehrmaßnahmen dagegen einzurichten. (Das können ganz einfache Abwehrmaßnahmen wie die Verschiebung einiger Daten, aber auch kompliziertere wie CFI-Techniken [Control Flow Integrity] sein.) Da es in vielschichtigem Code wie etwa dem von Windows Tausende von Schwachstellen geben kann, es aber nur eine begrenzte Menge von Angriffstechniken gibt, besteht das Ziel darin, die Ausnutzung ganzer Klassen von Bugs zu erschweren (oder gar unmöglich zu machen), anstatt zu versuchen, sämtliche Bugs zu finden. Schutz gegen Exploits Kapitel 7 855
Abwehrmaßnahmen auf Prozessebene Anwendungen können eigene Abwehrmaßnahmen gegen Exploits durchführen. So verwendet beispielsweise Microsoft Edge MemGC als Schutz gegen viele Arten von Speicherbeschädigungsangriffen. In diesem Abschnitt wollen wir uns aber mit den Abwehrmaßnahmen beschäftigen, die das Betriebssystem für alle Anwendungen oder für sich selbst einsetzt. Tabelle 7–20 führt alle Abwehrmaßnahmen der neuesten Version von Windows 10 Creators Update auf und gibt dabei jeweils die Art der Bugs an, gegen deren Ausnutzung sie schützen, und den Mechanismus zu ihrer Aktivierung. Maßnahme Zweck Aktivierung ASLR Bottom Up Randomization Unterwirft Aufrufe von VirtualAlloc einer ASLR mit 8-Bit-Entropie einschließlich Stackbasisrandomisierung. Wird mit dem Prozesserstellungs-­ Attributflag PROCESS_CREATION_MITIGATION_POLICY_BOTTOM_UP_ASLR_ALWAYS_ON eingerichtet. ASLR Force Relocate Images Erzwingt ASLR selbst für Binärdateien ohne das Linkerflag /DYNAMICBASE. Wird mit dem Prozesserstellungsflag PROCESS_CREATION_MITIGATION_POLICY_­ FORCE_RELOCATE_IMAGES_ALWAYS_ON eingerichtet. High Entropy ASLR (HEASLR) Sorgt für eine erhebliche Erhöhung der ASLR-Entropie für 64-Bit-Abbilder. Die Varianz der Bottom-Up-Randomisierung wird auf bis zu 1 TB heraufgesetzt (d. h., Bottom-Up-Zuweisungen können irgendwo zwischen 64 KB und 1 TB im Adressraum beginnen, was eine Entropie von 24 Bit ergibt). Muss zur Verlinkungszeit mit /HIGHENTROPYVA oder mit dem Prozesserstellungs-Attributflag PROCESS_­ CREATION_MITIGATION_POLICY_HIGH_­ ENTROPY_ASLR_ALWAYS_ON eingerichtet werden. ASLR Disallow Stripped Images Verhindert in Kombination mit ASLR Force Relocate Images das Laden jeglicher Bibliotheken ohne Relokalisierung (verlinkt mit dem Flag /FIXED) Wird mit SetProcessMitigationPolicy oder mit dem Prozesserstellungsflag PROCESS_CREATION_MITIGATION_POLICY_­ FORCE_RELOCATE_IMAGES_ALWAYS_ON_REQ_ RELOCS eingerichtet. DEP: Permanent Verhindert, dass ein Prozess die Datenausführungsverhinderung (DEP) für sich selbst ausschaltet. Betrifft nur 32-Bit-Anwendungen (auch WoW64). Wird mit dem Prozesserstellungsattribut SetProcessMitigationPolicy oder mit SetProcessDEPPolicy eingerichtet. DEP: Disable ATL Thunk Emulation Verhindert, dass veralteter ATL-Bibliothekscode ATL-Thunks auf dem Heap ausführt, selbst wenn ein bekanntes Kompatibilitätsproblem vorliegt. Betrifft nur 32-Bit-Anwendungen (auch WoW64). Wird mit dem Prozesserstellungsattribut SetProcessMitigationPolicy oder mit SetProcessDEPPolicy eingerichtet. SEH Overwrite Protection (SEHOP) Verhindert, das Strukturausnahmehandler durch falsche Versionen überschrieben werden, selbst wenn die Verlinkung nicht mit Safe SEH (/SAFESEH) erfolgte. Betrifft nur 32-Bit-Anwendungen (auch WoW64). Kann mit SetProcessDEPPolicy oder mit dem Prozesserstellungsflag PROCESS_ CREATION_MITIGATION_POLICY_SEHOP_­ ENABLE eingerichtet werden. 856 Kapitel 7 Sicherheit →
Maßnahme Zweck Aktivierung Raise Excep­ tion on Invalid Handle Hilft dabei, Angriffe durch Handlewiederverwendung (Nutzung eines Han­ dles nach dem Schließen) zu erkennen, bei denen ein Prozess ein Handle verwendet, das nicht mehr das erwartete ist (z. B. SetEvent für ein Mutex). Dazu wird der Prozess zum Absturz gebracht, anstatt einfach einen Fehler zurückzugeben, den der Prozess auch ignorieren kann. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY_­ STRICT_HANDLE_CHECKS_ALWAYS_ON eingerichtet. Raise Exception on Invalid Han­ dle Close Hilft dabei, Angriffe durch Handlewiederverwendung (zweifaches Schließen eines Handles) zu erkennen, bei denen ein Prozess ein Handle zu schließen versucht, das bereits geschlossen war. Bei dieser Maßnahme wird angeregt, ein anderes Handle zu verwenden, das die allgemeine Wirksamkeit dieses Exploits einschränkt. Nicht dokumentiert; kann auch nur mithilfe einer nicht dokumentierten API eingerichtet werden. Disallow Win32k System Calls Deaktiviert den gesamten Zugriff auf den Kernelmodus-Teilsystemtreiber Win32, der den Fenster-Manager (GUI), Graphics Device Interface (GDI) und DirectX implementiert. Für diese Komponente sind keine Systemaufrufe mehr möglich. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY_ WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON eingerichtet. Filter Win32k System Calls Schränkt den Zugriff auf den Kernelmodus-Teilsystemtreiber Win32k auf bestimmte APIs ein, sodass nur noch ein einfacher GUI- und DirectX-Zugriff möglich ist. Dies schützt gegen viele mögliche Angriffe, ohne die Verfügbarkeit der GUI/GDI-Dienste vollständig aufzuheben. Wird durch ein internes Prozesserstellungs-Attributflag eingerichtet, mit dem einer von drei möglichen Filtersätzen für Win32k aktiviert wird. Da die Filtersätze hartcodiert sind, ist diese Abwehrmaßnahme für den internen Gebrauch durch Microsoft reserviert. Disable Exten­ sion Points Hindert Prozesse daran, einen Eingabemethoden-Editor (IME), eine WindowsHook-DLL (SetWindowsHookEx), eine DLL zur Anwendungsinitialisierung (Registrierungswert AppInitDlls) oder einen geschichteten Winsock-Dienstanbieter (LSP) zu laden. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY_­ EXTENSION_POINT_DISABLE_ALWAYS_ON eingerichtet. Arbitrary Code Guard (CFG) Hindert Prozesse daran, ausführbaren Code zuzuweisen oder die Berechtigungen von bereits vorhandenem ausführbarem Code zu ändern, um ihn schreibbar zu machen. Diese Maßnahme kann so konfiguriert werden, dass ein bestimmter Thread in einem Prozess die entsprechende Fähigkeit anfordern oder ein Remoteprozess die Maßnahme abschalten kann. Aus Sicherheitsgründen sind letztere Möglichkeiten jedoch nicht empfehlenswert. Wird mit SetProcessMitigationPolicy oder den Prozesserstellungs-Attributflags PROCESS_CREATION_MITIGATION_POLICY_­ PROHIBIT_DYNAMIC_CODE_ALWAYS_ON und PROCESS_CREATION_MITIGATION_POLICY_­ PROHIBIT_DYNAMIC_CODE_ALWAYS_ON_­ ALLOW_OPT_OUT eingerichtet. Schutz gegen Exploits → Kapitel 7 857
Maßnahme Zweck Aktivierung Control Flow Guard (CFG) Hilft zu verhindern, dass Schwachstellen im Speicher ausgenutzt werden, um den Steuerungsfluss zu kapern. Dazu wird das Ziel aller indirekten CALL- und JMP-Anweisungen mit einer Liste gültiger, erwarteter Zielfunktionen verglichen. Diese Maßnahme gehört zum CFI-Mechanismus (Control Flow Integrity), der im nächsten Abschnitt beschrieben wird. Das Abbild muss mit der Option / guard:cf kompiliert und verlinkt werden. Wenn das bei dem Abbild nicht möglich ist, kann diese Schutzmaßnahme auch mit dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY_­ CONTROL_FLOW_GUARD_ALWAYS_ON eingerichtet werden. Die Durchsetzung von CFG ist jedoch nach wie vor für andere Abbilder wünschenswert, die in den Prozess geladen werden. CFG Export Suppression Verstärkt CFG, indem indirekte Aufrufe der exportierten API-Tabelle des Abbilds unterdrückt werden. Das Abbild muss mit /guard: ­exportsuppress kompiliert werden. Die Maßnahme kann aber auch durch ­SetProcessMitigationPolicy oder durch das Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_ POLICY_­CONTROL_FLOW_GUARD_EXPORT_­ SUPPRESSION eingerichtet werden. CFG Strict Mode Verhindert, dass im aktuellen Prozess Abbildbibliotheken geladen werden, die nicht mit der Option /guard:cf verlinkt wurden. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY2_ STRICT_CONTROL_FLOW_GUARD_ALWAYS_ON eingerichtet. Disable Non System Fonts Verhindert das Laden von Schriftartdateien, die nach der Installation im Verzeichnis C:\Windows\Fonts nicht bei der Anmeldung des Benutzers durch Winlogon registriert wurden. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY_ FONT_DISABLE_ALWAYS_ON eingerichtet. Microsoft-­ Signed Binaries Only Verhindert, dass im aktuellen Prozess Abbildbibliotheken geladen werden, die kein von einer Microsoft-Zertifizierungsstelle signiertes Zertifikat haben. Wird beim Start mit dem Prozessattributflag PROCESS_CREATION_MITIGATION_­ POLICY_BLOCK_NON_MICROSOFT_BINARIES_­ ALWAYS_ON festgelegt. Store-Signed Binaries Only Verhindert, dass im aktuellen Prozess Abbildbibliotheken geladen werden, die nicht von der Zertifizierungsstelle des Microsoft Stores signiert sind. Wird beim Start mit dem Prozessattributflag PROCESS_CREATION_MITIGATION_­ POLICY_BLOCK_NON_MICROSOFT_BINARIES_­ ALLOW_STORE festgelegt. No Remote Images Verhindert, dass im aktuellen Prozess Abbildbibliotheken geladen werden, die einen nicht lokalen Pfad (UNC oder WebDAV) aufweisen. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY_­ IMAGE_LOAD_NO_REMOTE_ALWAYS_ON eingerichtet. No Low IL Images Verhindert, dass im aktuellen Prozess Abbildbibliotheken geladen werden, deren verbindliche Beschriftung nicht mindestens Medium (0x2000) ist. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY_­ IMAGE_LOAD_NO_LOW_LABEL_ALWAYS_ON eingerichtet. Kann auch mit dem Ressourcenanspruch IMAGELOAD des Zugriffssteuerungseintrags für die Datei des zu ladenden Prozesses festgelegt werden. → 858 Kapitel 7 Sicherheit
Maßnahme Zweck Aktivierung Prefer System32 Images Ändern des Suchpfads des Loaders, sodass er unabhängig vom aktuellen Suchpfad immer in %SystemRoot%\ System32 nach der (durch einen relativen Namen angegebenen) zu ladenden Abbildbibliothek sucht. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY_ IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_ON eingerichtet. Return Flow ­Guard (RFG) Hilft, weitere Arten von Speicherschwachstellen zu verhindern, die sich auf den Steuerungsfluss auswirken können. Dazu wird vor der Ausführung einer RET-Anweisung geprüft, ob die Funktion von einem ROP-Exploit (Return-Oriented Programming) aufgerufen wurde, was daran zu erkennen ist, dass die Ausführung nicht korrekt begonnen wurde oder dass sie auf einem ungültigen Stack ausgeführt wird. Diese Maßnahme ist Teil des CFI-Mechanismus (Control Flow Integrity). Diese Abwehrmaßnahme ist zurzeit noch nicht verfügbar, da noch an einer stabilen Implementierung gearbeitet wird, die die Leistung nicht beeinträchtigt. Sie ist hier der Vollständigkeit halber angegeben. Restrict Set Thread Context Schränkt Änderungen am Kontext des aktuellen Threads ein. Zurzeit deaktiviert, bis RFG verfügbar ist, das diese Abwehrmaßnahme stabiler machen wird. Diese Maßnahme kann in zukünftigen Windows-Versionen enthalten sein. Sie ist hier der Vollständigkeit halber angegeben. Loader Continuity Verhindert, dass ein Prozess DLLs dynamisch lädt, die nicht dieselbe Integritätsebene wie der Prozess selbst haben. Dies wird in Fällen verwendet, in denen die zuvor genannte Signierrichtlinie aufgrund von Kompatibilitätsproblemen beim Start nicht aktiviert werden kann. Diese Maßnahme dient gezielt zur Abwehr von DLL-Planting-Angriffen. Wird mit SetProcessMitigationPolicy oder dem Prozesserstellungs-Attributflag PROCESS_CREATION_MITIGATION_POLICY2_­ LOADER_INTEGRITY_CONTINUITY_ALWAYS_ ON eingerichtet. Heap Terminate On Corruption Deaktiviert den fehlertoleranten Heap (FTH), sodass bei einer Heapbeschädigung keine Ausnahme ausgelöst wird, die ein Fortfahren erlaubt, sondern der Prozess beendet wird. Dadurch wird verhindert, dass ein Angreifer eine Heapbeschädigung ausnutzt, um einen von ihm selbst kontrollierten Ausnahmehandler auszuführen. Dies gilt auch in Fällen, in denen das Programm die Heapausnahme ignoriert oder in denen der Exploit nur manchmal eine Heap­ beschädigung verursacht. Dadurch wird die allgemeine Effektivität und Zu­ verlässigkeit des Exploits beschränkt. Wird mit HeapSetInformation oder dem Prozesserstellungs-Attributflag PROCESS_ CREATION_MITIGATION_POLICY_HEAP_­ TERMINATE_ALWAYS_ON eingerichtet. Schutz gegen Exploits → Kapitel 7 859
Maßnahme Zweck Aktivierung Disable Child Process Creation Verhindert das Erstellen von Kindprozessen, indem das Token mit einer besonderen Einschränkung gekennzeichnet wird, die alle anderen Komponenten davon abhält, einen Prozess anzulegen, während sie die Identität des Tokens für diesen Prozess annehmen (z. B. die Prozesserstellung durch WMI oder eine Kernelkomponente). Wird mit dem Prozesserstellungs-Attri­ butflag PROCESS_CREATION_CHILD_­ PROCESS_RESTRICTED eingerichtet und kann mit PROCESS_CREATION_DESKTOP_ APPX_OVERRIDE überschrieben werden, um Paketanwendungen (UWP-Anwendungen) zuzulassen. All Application Packages Policy Verhindert, dass eine Anwendung in einem Anwendungscontainer auf Ressourcen mit der SID ALL APPLICATION PACKAGES zugreift (siehe den Abschnitt »Anwendungscontainer« weiter vorn in diesem Kapitel). Stattdessen muss die SID ALL RESTRICTED APPLICATION PACKAGES vorhanden sein. Manchmal wird diese Maßnahme auch als Less Privileged App Container (LPAC) bezeichnet. Wird mit dem Prozesserstellungs-Attributflag PROC_THREAD_ATTRIBUTE_ALL_­ APPLICATION_PACKAGES_POLICY eingerichtet. Tabelle 7–20 Abwehrmaßnahmen auf Prozessebene Es ist auch möglich, einige dieser Maßnahmen ohne Mitwirkung des Entwicklers anwendungsoder systemweit zu aktivieren. Öffnen Sie dazu den lokalen Gruppenrichtlinien-Editor und erweitern Sie wie in Abb. 7–30 gezeigt Computerkonfiguration > Administrative Vorlagen > System > Ausgleichsoptionen (womit die Optionen für die in diesem Abschnitt beschriebenen Abwehrmaßnahmen gemeint sind, die in der englischen Oberfläche als »mitigations« bezeichnet werden). Geben Sie im Dialogfeld Optionen für den Prozessausgleich den passenden Bitwert für die zu aktivierenden Abwehrmaßnahmen ein, wobei die einzelnen Maßnahmen mit einer 1 aktiviert und mit einer 0 deaktiviert werden. Bei Eingabe von ? wird die Standardeinstellung oder der vom Prozess angeforderte Wert verwendet (siehe Abb. 7–30). Die Bitzahlen sind der Auflistung PROCESS_MITIGATION_POLICY in der Headerdatei Winnt.h entnommen. Die Eingaben führen dazu, dass der entsprechende Registrierungswert in den IFEO-Schlüssel (Image File Extension Options) für den eingegebenen Abbildnamen geschrieben wird. In der aktuellen Version von Windows 10 Creators Update und den früheren Versionen werden dabei leider viele der neueren Abwehrmaßnahmen entfernt. Um das zu verhindern, setzen Sie in der Registrierung den DWORD-Wert MitigrationOptions. 860 Kapitel 7 Sicherheit
Abbildung 7–30 Einstellen der Optionen für Abwehrmaßnahmen auf Prozessebene Control Flow Integrity Die Datenausführungsverhinderung (Data Execution Prevention, DEP) und Arbitrary Code ­Guard (ACG) machen es für Exploits schwerer, ausführbaren Code auf den Heap oder Stack zu legen, neuen ausführbaren Code zuzuweisen oder vorhandenen ausführbaren Code zu ändern. Daher sind für Angreifer reine Speicher/Daten-Techniken interessanter geworden. Dabei werden Teile des Arbeitsspeichers manipuliert, etwa die Rücksprungadresse auf dem Stack oder indirekte Funktionszeiger im Speicher, um den Steuerungsfluss umzuleiten. Häufig werden Techniken wie ROP und JOP (Return- bzw. Jump-Oriented Programming) eingesetzt, um in den normalen Steuerungsfluss des Programms einzugreifen und ihn zu bekannten Speicherorten von besonderen Codeteilen (»Gadgets«) umzuleiten, die für den Angreifer nutzbar sind. Da sich solche Codeausschnitte in der Mitte oder am Ende verschiedener Funktionen befinden, erfolgt die Codeumleitung in die Mitte oder an das Ende legitimer Funktionen. Mithilfe von CGI-Techniken (Control Flow Integrity) können das Betriebssystem und der Compiler die meisten Arten solcher Exploits erkennen und verhindern. Dabei wird beispielsweise überprüft, ob das Ziel einer indirekten JMP- oder CALL-Anweisung der Beginn einer echten Funktion ist, ob eine RET-Anweisung auf einen erwarteten Speicherort zeigt oder ob sie nach dem Eintritt in die Funktion ausgegeben wurde. Schutz gegen Exploits Kapitel 7 861
Ablaufsteuerungsschutz Der Ablaufsteuerungsschutz (Control Flow Guard, CFG) ist ein Schutzmechanismus gegen Exploits, der mit Windows 8.1 Update 3 eingeführt wurde und in erweiterter Form auch in Windows 10 und Windows Server 2016 vorhanden ist, wobei im Verlaufe der Updates weitere Verbesserungen hinzugekommen sind (einschließlich des letzten Creators Updates). Er war ursprünglich nur für Benutzermoduscode gedacht, doch mit dem Creators Update kam auch Kernel CFG (KCFG) hinzu. Diese Einrichtung prüft, ob das Ziel eines indirekten CALL- oder JMP-Aufrufs der Beginn einer bekannten Funktion ist (was das genau bedeutet, erfahren Sie in Kürze). Ist das nicht der Fall, wird der Prozess einfach beendet. Abb. 7–31 zeigt einen Überblick über die Funktionsweise von CFG. Vor einem indirekten Aufruf Vom Compiler generierter Code Betriebssystemcode Zieladresse speichern Validierungsfunktion aufrufen Ist das Ziel gültig? N Prozess beenden J Aufruf durchführen Ausführung fortsetzen Abbildung 7–31 Funktionsprinzip des Ablaufsteuerungsschutzes Für CFG ist die Mitarbeit eines kompatiblen Compilers erforderlich, der vor indirekten Änderungen am Steuerungsfluss einen Aufruf des Validierungscodes hinzufügt. Der Visual C++-Compiler verfügt über die Option /guard:cf, die für Abbilder (oder sogar C/C++-Quelldateien) gesetzt werden muss, um das Programm mit CFG-Unterstützung zu erstellen. (Diese Option steht auch in der grafischen Benutzeroberfläche von Visual Studio in der Einstellung C/C++ > Codegenerierung > Ablaufsteuerungsschutz der Projekteigenschaften zur Verfügung.) Dies muss auch in den Linkereinstellungen festgelegt werden, da zur Unterstützung von CFG beide Komponenten von Visual Studio zusammenwirken müssen. EXE- und DLL-Dateien, die mit CFG-Unterstützung kompiliert wurden, zeigen dies im PE-Header an. Außerdem enthalten sie eine Liste der Funktionen, die gültige Ziele für indirekte Aufrufe sind. Diese Liste befindet sich im PE-Abschnitt .gfids, der vom Linker normalerweise mit dem Abschnitt .rdata verschmolzen wird. Sie wird vom Linker erstellt und enthält die relativen virtuellen Adressen (RVA) aller Funktionen in dem Abbild. Dazu gehören auch solche, die von dem Code in dem Abbild nicht indirekt aufgerufen werden, da es schließlich keine Möglich- 862 Kapitel 7 Sicherheit
keit gibt, um zu bestimmen, ob externer Code auf legitime Weise die Adresse einer Funktion herausfinden und versuchen kann, sie aufzurufen. Das gilt insbesondere für exportierte Funktionen, die von Code aufgerufen werden können, nachdem er ihren Zeiger mit GetProcAddress ermittelt hat. Allerdings können Programmierer die Technik der sogenannten CFG-Unterdrückung anwenden, die über die Anmerkung DECLSPEC_GUARD_SUPRESS zur Verfügung steht. Sie kennzeichnet eine Funktion der Tabelle der gültigen Funktionen mit einem besonderen Flag, um anzuzeigen, dass der Programmierer es für ausgeschlossen hält, dass die Funktion jemals das Ziel eines indirekten Aufrufs oder Sprungs wird. Eine einfache Validierungsfunktion muss nun nur noch das Teil einer CALL- oder JMP-Anweisung mit den Einträgen in der Tabelle gültiger Zielfunktionen vergleichen. Dazu ist ein Algorithmus der Ordnung O(n) erforderlich, bei dem die Anzahl der zu überprüfenden Funktionen im schlimmsten Fall gleich der Anzahl der Funktionen in der Tabelle ist. Die lineare Durchsuchung eines Arrays bei jeder einzelnen indirekten Änderung des Steuerungsflusses würde das Programm natürlich in die Knie zwingen. Daher ist eine Unterstützung durch das Betriebssystem erforderlich, um die CFG-Prüfung effizient durchzuführen. Wie Windows das erreicht, sehen wir uns im nächsten Abschnitt an. Experiment: Informationen über den Ablaufsteuerungsschutz Mit dem Visual Studio-Tool DumpBin können Sie sich einige grundlegende Informationen über den Ablaufsteuerungsschutz anzeigen lassen. Im Folgenden sehen Sie, wie Sie die Angaben über die Header und die Laderkonfiguration für Smss ausgeben: c:\> dumpbin /headers /loadconfig c:\windows\system32\smss.exe Microsoft (R) COFF/PE Dumper Version 14.00.24215.1 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file c:\windows\system32\smss.exe PE signature found File Type: EXECUTABLE IMAGE FILE HEADER VALUES 8664 machine (x64) 6 number of sections 57899A7D time date stamp Sat Jul 16 05:22:53 2016 0 file pointer to symbol table 0 number of symbols F0 size of optional header 22 characteristics Executable Application can handle large (>2GB) addresses → Schutz gegen Exploits Kapitel 7 863
OPTIONAL HEADER VALUES 20B magic # (PE32+) 14.00 linker version 12800 size of code EC00 size of initialized data 0 size of uninitialized data 1080 entry point (0000000140001080) NtProcessStartupW 1000 base of code 140000000 image base (0000000140000000 to 0000000140024FFF) 1000 section alignment 200 file alignment 10.00 operating system version 10.00 image version 10.00 subsystem version 0 Win32 version 25000 size of image 400 size of headers 270FD checksum 1 subsystem (Native) 4160 DLL characteristics High Entropy Virtual Addresses Dynamic base NX compatible Control Flow Guard ... Section contains the following load config: 000000D0 size 0 time date stamp 0.00 Version 0 GlobalFlags Clear 0 GlobalFlags Set 0 Critical Section Default Timeout 0 Decommit Free Block Threshold 0 Decommit Total Free Threshold 0000000000000000 Lock Prefix Table 0 Maximum Allocation Size 0 Virtual Memory Threshold 0 Process Heap Flags 0 Process Affinity Mask 0 CSD Version 0800 Dependent Load Flag → 864 Kapitel 7 Sicherheit
0000000000000000 Edit List 0000000140020660 Security Cookie 00000001400151C0 Guard CF address of check-function pointer 00000001400151C8 Guard CF address of dispatch-function pointer 00000001400151D0 Guard CF function table 2A Guard CF function count 00010500 Guard Flags CF Instrumented FID table present Long jump target table present 0000 Code Integrity Flags 0000 Code Integrity Catalog 00000000 Code Integrity Catalog Offset 00000000 Code Integrity Reserved 0000000000000000 Guard CF address taken IAT entry table 0 Guard CF address taken IAT entry count 0000000000000000 Guard CF long jump target table 0 Guard CF long jump target count 0000000000000000 Dynamic value relocation table Guard CF Function Table Address -------0000000140001010 _TlgEnableCallback 0000000140001070 SmpSessionComplete 0000000140001080 NtProcessStartupW 0000000140001B30 SmscpLoadSubSystemsForMuSession 0000000140001D10 SmscpExecuteInitialCommand 0000000140002FB0 SmpExecPgm 0000000140003620 SmpStartCsr 00000001400039F0 SmpApiCallback 0000000140004E90 SmpStopCsr ... Informationen im Zusammenhang mit dem Ablaufsteuerungsschutz sind in der vorstehenden Ausgabe fett markiert und werden in Kürze ausführlicher erläutert. Öffnen Sie zunächst aber Process Explorer, rechtsklicken Sie auf den Kopf der Spalte Process und wählen Sie Select Columns. Aktivieren Sie dann auf der Registerkarte Process Image das Kontrollkästchen Control Flow Guard und auf der Registerkarte Process Memory das Kontrollkästchen Virtual Size. Daraufhin erhalten Sie eine Anzeige wie die folgende: → Schutz gegen Exploits Kapitel 7 865
Wie Sie sehen, wurden die meisten von Microsoft gelieferten Prozesse mit CFG erstellt (darunter Smss, Csrss, Audiodg, Notepad usw.). Die virtuelle Größe dieser CFG-Prozesse ist erstaunlich hoch. Denken Sie aber daran, dass die virtuelle Größe den gesamten in dem Prozess verwendeten Adressraum angibt, unabhängig davon, ob der Speicher zugesichert oder reserviert ist. Dagegen zeigt die Spalte Private Bytes den privaten zugesicherten Speicher, dessen Umfang nicht einmal in die Nähe der virtuellen Größe kommt (obwohl die virtuelle Größe auch den nicht privaten Speicher einschließt). Für 64-Bit-Prozesse beträgt die virtuelle Größe mindestens 2 TB. Den Grund dafür werden Sie in Kürze kennenlernen. Die CFG-Bitmap Wie Sie bereits gesehen haben, wäre es nicht praktikabel, das Programm alle paar Anweisungen dazu zu zwingen, eine Liste von Funktionsaufrufen zu durchsuchen. Aus Leistungsgründen muss statt eines linearen O(n)-Algorithmus ein Algorithmus der Ordnung O(1) verwendet werden, also einer, bei dem das Nachschlagen unabhängig von der Anzahl der Funktionen in der Tabelle immer gleich lang dauert. Dabei sollte die Nachschlagezeit so kurz wie möglich sein. Ein guter Kandidat dafür wäre ein Array, das nach den Adressen der Zielfunktionen indiziert wird und einfach angibt, ob die einzelnen Adressen gültig sind oder nicht (z. B. ein einfacher BOOL-Typ). Bei 128 TB an möglichen Adressen wäre ein solches Array jedoch selbst 128 TB * sizeof(BOOL) groß, was für eine Speicherung nicht akzeptabel ist – denn das wäre größer als der Adressraum! Gibt es eine bessere Möglichkeit? Zunächst einmal können wir die Tatsache ausnutzen, dass Compiler x64-Funktionscode in 16-Byte-Grenzen erstellen müssen. Das verringert die Größe des erforderlichen Arrays auf nur noch 8 TB * sizeof(BOOL). Aber einen kompletten BOOL-Typ zu verwenden (der schlimmstenfalls vier Bytes und bestenfalls ein Byte umfasst), stellt eine ziemliche Verschwendung dar. Schließlich brauchen wir nur einen Status – gültig oder ungültig –, der nur ein Bit belegt. Da- 866 Kapitel 7 Sicherheit
durch erhalten wir 8 TB/8, also einfach 1 TB. Doch leider gibt es bei der Sache einen Haken: Es gibt keine Garantie dafür, dass der Compiler alle Funktionen an 16-Byte-Grenzen ausrichtet. Handgemachter Assemblycode und einzelne Optimierungen können diese Regel verletzen. Daher müssen wir eine Lösung finden. Eine Möglichkeit besteht darin, einfach ein anderes Bit zu verwenden, um anzuzeigen, dass die Funktion irgendwo in den nächsten 15 Bytes beginnt und nicht unmittelbar an der 16-Byte-Grenze. Dabei haben wir die folgenden Möglichkeiten: QQ {0, 0} Keine gültige Funktion beginnt innerhalb dieser 16-Byte-Grenze. QQ {1, 0} Gültige Funktionen beginnen genau an der 16-Byte-Grenze. QQ {1, 1} Gültige Funktionen beginnen irgendwo innerhalb einer 16-Byte-Adresse. Wenn ein Angreifer versucht, einen Aufruf innerhalb einer Funktion vorzunehmen, die vom Linker als 16-Byte-ausgerichtet gekennzeichnet wurde, ergibt sich der 2-Bit-Status {1, 0}, doch da die Adresse nicht an den 16-Byte-Grenzen ausgerichtet ist, sind die erforderlichen Bits (die Bits 3 und 4) in der Adresse {1, 1}. Daher kann ein Angreifer nur dann eine willkürliche Anweisung in den ersten 16 Bytes der Funktion aufrufen, wenn der Linker die Funktion nicht als ausgerichtet erstellt hat (wobei die Bits wie gezeigt {1, 1} wären. Aber selbst in diesem Fall muss die Anweisung auch noch für den Angreifer von Nutzen sein, ohne die Funktion zum Absturz zu bringen (gewöhnlich eine Art von Stackpivot oder Gadget, das mit einer ret-Anweisung endet). Damit können wir nun die folgenden Formeln anwenden, um die Größe der CFG-Bitmap zu berechnen: QQ 32-Bit-Anwendung auf x86 oder x64 2 GB / 16 * 2 = 32 MB QQ 32-Bit-Anwendung mit /LARGEADDRESSAWARE, gestartet im 3-GB-Modus auf x86 3 GB / 16 * 2 = 48 MB QQ 64-Bit-Anwendung 128 TB / 16 * 2 = 2 TB QQ 32-Bit-Anwendung mit /LARGEADDRESSAWARE auf x64 4 GB / 16 * 2 = 64 MB zzgl. der Größe der 64-Bit-Bitmap, die zum Schutz von 64-Bit-Ntdll.dll- und WoW64-Komponenten benötigt wird, also 2 TB + 64 MB Es ist jedoch immer noch eine ziemliche Leistungsbremse, bei der Ausführung jedes einzelnen Prozesses 2 TB zuzuweisen und auszufüllen. Wir haben die Kosten für indirekte Aufrufe zwar fixiert, aber es ist nicht akzeptabel, dass der Start eines Prozesses so lange dauert. Außerdem würden 2 TB an zugesichertem Speicher sehr schnell an den Zusicherungsgrenzwert stoßen. Daher werden noch zwei Tricks angewendet, um Speicher zu sparen und die Leistung zu steigern. Erstens reserviert der Speicher-Manager nur die Bitmap, wobei er davon ausgeht, dass die CFG-Validierungsfunktion eine Ausnahme beim Zugriff auf die Bitmap als ein Zeichen für den Bitstatus {0, 0} deutet. Wenn eine Region 4 KB an Bitstatus enthält, die alle {0, 0} sind, kann sie reserviert belassen werden. Nur Seiten, auf denen mindestens ein Bit auf {1, X} gesetzt ist, müssen zugesichert werden. Wie im Abschnitt über ASLR in Kapitel 5 beschrieben, führt das System die Randomisierung und Relokalisierung von Bibliotheken gewöhnlich nur beim Start durch, um sich wiederholte Relokalisierungen zu ersparen und damit die Leistung zu steigern. Wenn eine Bibliothek, die ASLR unterstützt, einmal an einer gegebenen Adresse geladen wurde, wird sie daher stets Schutz gegen Exploits Kapitel 7 867
an dieser Adresse geladen. Das bedeutet auch, dass die einmal berechneten Bitmapstatus für die Funktionen in dieser Bibliothek in allen anderen Prozessen, die dieselbe Binärdatei laden, identisch sind. Daher behandelt der Speicher-Manager die CFG-Bitmap als eine auslagerungsgepufferte, gemeinsam nutzbare Speicherregion, wobei die physischen Seiten, die den gemeinsam genutzten Bits entsprechen, nur einmal im RAM vorhanden sind. Das verringert den Umfang der zugesicherten Seiten im RAM. Nur Bits, die zu privatem Speicher gehören, müssen berechnet werden. In regulären Anwendungen ist privater Speicher normalerweise nicht ausführbar, sondern nur beim »Kopieren beim Schreiben«, wenn jemand eine Bibliothek gepatcht hat (was jedoch beim Laden von Abbildern nicht passiert). Daher sind die Kosten beim Laden einer Anwendung, die die gleichen Bibliotheken verwendet wie bereits gestartete Anwendungen, fast null. Das folgende Experiment veranschaulicht dies. Experiment: CFG-Bitmap Öffnen Sie VMMap und markieren Sie einen Notepad-Prozess. Im Abschnitt Sharable sollte jetzt ein großer reservierter Block angezeigt werden: Wenn Sie den unteren Bereich der Größe nach sortieren, können Sie sehr schnell den großen Bereich für die CFG-Bitmap finden (siehe Abbildung). Sie können sich auch mit WinDbg in den Prozess einklinken und den Befehl !address eingeben, um sich die CFG-Bitmap anzeigen zu lassen: 7df5'ff530000 7df6'0118a000 0'01c5a000 MEM_MAPPED MEM_RESERVE Other [CFG Bitmap] → 868 Kapitel 7 Sicherheit
7df6'0118a000 7df6'011fb000 0'00071000 MEM_MAPPED MEM_COMMIT PAGE_NOACCESS Other [CFG Bitmap] 7df6'011fb000 7ff5'df530000 1ff'de335000 MEM_MAPPED MEM_RESERVE Other [CFG Bitmap] 7ff5'df530000 7ff5'df532000 0'00002000 MEM_MAPPED MEM_COMMIT PAGE_READONLY Other [CFG Bitmap] Zwischen den MEM_COMMIT-Regionen, bei denen mindestens ein gültiger Bitstatus gesetzt sein muss ({1, X}), stehen umfangreiche Regionen, die als MEM_RESERVE gekennzeichnet sein. Alle (oder zumindest fast alle) Regionen sind im Speicher zugeordnet (MEM_MAPPED), da sie zu der gemeinsam genutzten Bitmap gehören. Aufbau der CFG-Bitmap Bei der Initialisierung des Systems wird die Funktion MiInitializeCfg aufgerufen, um die CFG-Unterstützung zu initialisieren. Sie erstellt ein oder zwei Abschnittsobjekte (MmCreateSection) als reservierten Arbeitsspeicher mit der für die jeweilige Plattform geeigneten Größe (siehe die Berechnung weiter vorn). Auf 32-Bit-Plattformen reicht eine Bitmap aus, doch auf x64 sind zwei erforderlich, nämlich eine für 64-Bit- und die andere für WoW64-Prozesse (also 32-Bit-Anwendungen). Die Zeiger der Abschnittsobjekte werden in einer Teilstruktur innerhalb der globalen Variablen MiState gespeichert. Wenn ein Prozess erstellt wurde, wird der entsprechende Abschnitt auf sichere Weise dem Adressraum dieses Prozesses zugeordnet. »Auf sichere Weise« bedeutet hier, dass Code, der in dem Prozess ausgeführt wird, nicht in der Lage ist, die Zuordnung dieses Abschnitts zu ändern oder seine Schutzeinstellungen zu ändern. (Sonst könnte Schadcode einfach die Zuordnung des Speichers aufheben, ihn neu zuweisen und alles mit 1-Bits füllen, wodurch der Ablaufsteuerungsschutz aufgehoben würde, oder einfach die Region als schreibbar kennzeichnen, um beliebige Bits zu ändern.) Die CFG-Bitmaps für den Benutzermodus werden in folgenden Situationen gefüllt: QQ Bei der Zuordnung von Abbildern werden die Metadaten über die Ziele von indirekten Aufrufen von Abbildern extrahiert, die aufgrund der ASLR (siehe Kapitel 5) dynamisch relokalisiert wurden. Verfügt ein Abbild nicht über solche Metadaten – wurde es also nicht mit CFG kompiliert –, so wird angenommen, dass alle in ihm enthaltenen Adressen indirekt aufgerufen werden können. Dynamisch relokalisierte Abbilder werden wie bereits erwähnt in jedem Prozess an derselben Adresse geladen. Daher werden ihre Metadaten dazu herangezogen, den gemeinsam genutzten Abschnitt zu füllen, der für die CFG-Bitmap verwendet wird. QQ Bei der Zuordnung von Abbildern verlangen Abbilder, die nicht dynamisch relokalisiert oder nicht an ihrer bevorzugten Basisadresse zugeordnet wurden, besondere Aufmerksamkeit. Bei ihrer Zuordnung werden die entsprechenden Seiten der CFG-Bitmap privat gemacht und mithilfe der CFG-Metadaten aus dem Abbild gefüllt. Bei Abbildern, deren CFG-Bits in der gemeinsam genutzten CFG-Bitmap vorhanden sind, wird geprüft, ob alle Schutz gegen Exploits Kapitel 7 869
relevanten CFG-Bitmapseiten nach wie vor gemeinsam genutzt werden können. Wenn das nicht der Fall ist, werden die privaten CFG-Bitmapseiten mithilfe der CFG-Metadaten aus dem Abbild gefüllt. QQ Wird virtueller Speicher zugewiesen oder vorab als ausführbar geschützt, werden die entsprechenden Seiten der CFG-Bitmap privat gemacht und standardmäßig ausschließlich mit Einsen initialisiert. Das ist für Situationen wie die Just-in-Time-Kompilierung erforderlich, bei der Code im laufenden Betrieb generiert und dann ausgeführt wird (z. B. bei .NET oder Java). Verstärkung des CFG-Schutzes Der Ablaufsteuerungsschutz leistet zwar gute Arbeit, um Exploits zu verhindern, bei denen indirekte Aufrufe oder Sprünge ausgenutzt werden, kann aber auch umgangen werden: QQ Wenn der Prozess dazu verführt oder eine vorhandene JIT-Engine dazu missbraucht werden kann, ausführbaren Arbeitsspeicher zuzuweisen, dann sind alle zugehörigen Bits auf {1, 1} gesetzt, was den gesamten Speicher zu einem gültigen Aufrufziel macht. QQ Wenn das erwartete Aufrufziel bei einer 32-Bit-Anwendung __stdcall ist (Standardaufrufkonvention), der Angreifer es aber in __cdecl (C-Aufrufkonvention) ändern kann, dann wird der Stack beschädigt, da die C-Aufruffunktion im Gegensatz zur Standardaufruffunktion die Argumente des Aufrufers nicht aufräumt und der Ablaufsteuerungsschutz nicht zwischen den verschiedenen Aufrufkonventionen unterscheiden kann. Der Stack kann dabei eine vom Angreifer kontrollierte Rücksprungadresse aufweisen, wodurch der Ablaufsteuerungsschutz ausgehebelt wird. QQ Ähnliches gilt für vom Compiler generierte setjmp/longjmp-Ziele, die sich anders verhalten als echte indirekte Aufrufe. Auch hier kann CFG nicht zwischen den beiden Vorgängen unterscheiden. QQ Bestimmte indirekte Aufrufe lassen sich nicht so leicht schützen, beispielsweise die Importadresstabelle (IAT) oder die Delay-Load-Adresstabelle, die sich gewöhnlich in einem schreibgeschützten Abschnitt der ausführbaren Datei befindet. QQ Exportierte Funktionen sind unter Umständen keine wünschenswerten indirekten Funk­ tionsaufrufe. In Windows 10 wurden Verbesserungen am Ablaufsteuerungsschutz vorgenommen, um diese Probleme zu lösen. Die erste dieser Maßnahmen besteht in der Einführung zweier neuer Flags, nämlich PAGE_TARGETS_INVALID für die Funktion VirtualAlloc und PAGE_TARGETS_NO_UPDATE für VirtualProtect. Wenn diese Flags gesetzt sind, können JIT-Engines, die ausführbaren Speicher zuweisen, nicht alle Bits für ihre Zuweisung sehen, die auf {1, 1} gesetzt wurden. Stattdessen müssen diese Engines manuell die Funktion SetProcessValidCallTargets aufrufen (die ihrerseits die native Funktion NtSetInformationVirtualMemory aufruft), die es ihnen erlaubt, die tatsächlichen Funktionsstartadressen des von ihnen kompilierten Codes anzugeben. Außerdem wird diese Funktion mit DECLSPEC_GUARD_SUPPRESS als unterdrückter Aufruf gekennzeichnet, sodass Angreifer keinen indirekten Aufruf oder Sprung nutzen können, um dorthin umzuleiten, nicht einmal zu ihrem Start. (Da es sich um eine von Natur aus gefährliche Funktion handelt, 870 Kapitel 7 Sicherheit
kann ihr Aufruf mit einem vom Aufrufer kontrollierten Stack oder Registern zu einer Umgehung des Ablaufsteuerungsschutzes führen.) Die Verbesserungen am Ablaufsteuerungsschutz ändern auch den Standardablauf, den Sie zu Anfang dieses Abschnitts gesehen haben. Der Lader implementiert jetzt nicht mehr eine einfache Funktion nach dem Prinzip »Ziel prüfen, zurückspringen«, sondern »Ziel prüfen, Ziel aufrufen, Stack prüfen, zurückspringen«. Diese neue Funktion wird an einigen Stellen in 32-Bit-Anwendungen (auch unter WoW64) verwendet. Abb. 7–32 zeigt diesen verbesserten Ausführungsfluss. Vor einem indirekten Aufruf Vom Compiler generierter Code Betriebssystemcode Zieladresse speichern Ist das Ziel gültig? Validierungsfunktion aufrufen N Prozess beenden J Stackzeiger speichern Aufruf durchführen Ist der Stackzeiger derselbe? N J Ausführung fortsetzen Abbildung 7–32 Verbesserter Ablaufsteuerungsschutz Im Rahmen der Verbesserungen wurden auch zusätzliche Tabellen in die ausführbare Datei aufgenommen, etwa die Address-Taken-IAT-Tabelle und die Longjump-Adresstabelle. Wenn im Compiler der Longjump- und der IAT-CFG-Schutz aktiviert sind, werden in diesen Tabellen Zieladressen für die entsprechenden besonderen Arten von indirekten Aufrufen gespeichert. Die entsprechenden Funktionen stehen dann nicht in der regulären Funktionstabelle und werden daher auch nicht in der Bitmap erwähnt. Code, der versucht, eine dieser Funktionen indirekt anzuspringen oder aufzurufen, wird daher als illegaler Übergang behandelt. Um die entsprechenden Ziele zu validieren, also z. B. diejenigen der Funktion longjmp, überprüfen die C-Laufzeit und der Linker manuell die zugehörige Tabelle. Das ist zwar noch unwirtschaftlicher als die Durchsuchung der Bitmap, aber da diese Tabellen wenn überhaupt, dann nur sehr wenige Funktionen enthalten, ist dieser Mehraufwand tragbar. Schließlich wurde noch eine Exportunterdrückung eingeführt, die vom Compiler unterstützt und von einer Abwehrmaßnahme auf Prozessebene aktiviert werden muss (siehe den Schutz gegen Exploits Kapitel 7 871
Abschnitt »Abwehrmaßnahmen auf Prozessebene« weiter vorn in diesem Kapitel). Wenn diese Einrichtung eingeschaltet ist, wird der neue Bitstatus {0, 1} verwendet (der in der früheren Aufstellung nicht definiert war): Er gibt an, dass die Funktion gültig ist, aber einer Exportunterdrückung unterliegt, weshalb sie vom Lader besonders behandelt wird. Welche Schutzvorkehrungen in einer Binärdatei enthalten sind, können Sie an den Schutzflags in der IMAGE_LOAD_CONFIGURATION_DIRECTORY-Struktur ablesen, die die bereits vorgestellte Anwendung DumpBin entschlüsseln kann. Eine Übersicht über die Flags finden Sie in Tabelle 7–21. Flagsymbol Wert Beschreibung IMAGE_GUARD_CF_INSTRUMENTED 0x100 In dem Modul ist die Unterstützung für den Ablaufsteuerungsschutz vorhanden. IMAGE_GUARD_CFW_INSTRUMENTED 0x200 Das Modul führt den Ablaufsteuerungsschutz und Schreibintegritätsprüfungen durch. IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT 0x400 Das Modul enthält CFG-fähige Funk­ tionslisten. IMAGE_GUARD_SECURITY_COOKIE_UNUSED 0x800 Das Modul nutzt das vom Compilerflag /GS ausgegebene Sicherheitscookie nicht. IMAGE_GUARD_PROTECT_DELAYLOAD_IAT 0x1000 Das Modul unterstützt schreibgeschützte Delay-Load-Importadresstabellen (IAT). IMAGE_GUARD_DELAYLOAD_IAT_IN_ITS_OWN_­ SECTION 0x2000 Die Delay-Load-IAT befindet sich in ihrem eigenen Abschnitt und kann daher bei Bedarf erneut geschützt werden. IMAGE_GUARD_CF_EXPORT_SUPPRESSION_INFO_ PRESENT 0x4000 Das Modul enthält unterdrückte Export­informationen. IMAGE_GUARD_CF_ENABLE_EXPORT_SUPPRESSION 0x8000 Das Modul ermöglicht eine Exportunterdrückung. IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT 0x10000 Das Modul enthält Longjump-Zielinformationen. Tabelle 7–21 Flags für den Ablaufsteuerungsschutz Zusammenspiel von Lader und Ablaufsteuerungsschutz Es ist zwar der Speicher-Manager, der die CFG-Bitmap erstellt, doch auch der Benutzermodus-Lader (siehe Kapitel 3) spielt eine Rolle und hat zwei Aufgaben zu erfüllen. Die erste besteht darin, die CFG-Unterstützung dynamisch dann und nur dann zu aktivieren, wenn dieses Merkmal aktiviert ist. (Es kann beispielsweise sein, dass ein Aufrufer für den Kindprozess einen Verzicht auf den Ablaufsteuerungsschutz verlangt hat oder der Prozess selbst keine CFG-Unterstützung aufweist.) Dies erfolgt durch die Laderfunktion LdrpCfgProcessLoadConfig, die aufgerufen wird, um den Ablaufsteuerungsschutz für jedes geladene Modul zu initialisieren. Wenn das CFG-Flag (IMAGE_DLLCHARACTERISTICS_GUARD_CF) in den DllCharacteristics-Modulflags im optionalen PE-Header oder das Flag IMAGE_GUARD_CF_INSTRUMENTED im Element GuardFlags der 872 Kapitel 7 Sicherheit
IMAGE_LOAD_CONFIG_DIRECTORY-Struktur nicht gesetzt ist oder der Kernel den Ablaufsteuerungs- schutz für das Modul zwangsweise abgeschaltet hat, tut diese Funktion nichts. Verwendet das Modul den Ablaufsteuerungsschutz, so besteht die zweite Aufgabe darin, dass LdrpCfgProcessLoadConfig den Zeiger für die Überprüfungsfunktion für indirekte Aufrufe vom Abbild abruft (das Element GuardCFCheckFunctionPointer der IMAGE_LOAD_CONFIG_DIRECTORY-­ Struktur) und entweder auf LdrpValidateUserCallTarget oder LdrpValidateUserCallTargetES in Ntdll setzt, je nachdem, ob die Exportunterdrückung aktiviert ist oder nicht. Zuvor vergewissert sich die Funktion, dass der indirekte Zeiger nicht verändert wurde und auf eine Stelle außerhalb des Moduls zeigt. Wurde die Binärdatei mit verbessertem Ablaufsteuerungsschutz kompiliert, so steht darüber hinaus die CFG-Dispatchroutine als zweite Prüfroutine zur Verfügung, um den zuvor beschriebenen erweiterten Ausführungsfluss zu implementieren. Enthält das Abbild einen Zeiger auf eine solche Funktion (im Element GuardCFDispatchFunctionPointer in der bereits erwähnten Struktur), so wird er mit LdrpDispatchUserCallTarget bzw. bei aktivierter Exportunterdrückung mit LdrpDispatchUserCallTargetES initialisiert. In einigen Fällen kann der Kernel selbst indirekte Sprünge oder Aufrufe für den Benutzermodus durchführen. Wenn das möglich ist, implementiert der Kernel seine eigene MmValidateUserCallTarget-Routine, die die gleichen Aufgaben ausführt wie LdrpValidateUserCallTarget. In Code, der von einem Compiler mit aktiviertem Ablaufsteuerungsschutz generiert wurde, landen indirekte Aufrufe in der Ntdll-Funktion LdrpValidateCallTarget(ES) oder LdrpDispatch­ UserCallTarget(ES), die anhand der Zielverzweigungsadresse den Bitstatuswert für die Funktion prüft: QQ Ist der Bitstatus {0, 0}, so ist der Aufruf möglicherweise ungültig. QQ Ist der Bitstatus {1, 0} und die Adresse an 16-Byte-Grenzen ausgerichtet, so ist der Aufruf gültig. Anderenfalls ist er möglicherweise ungültig. QQ Ist der Bitstatus {1, 1} und die Adresse nicht an 16-Byte-Grenzen ausgerichtet, so ist der Aufruf gültig. Anderenfalls ist er möglicherweise ungültig. QQ Ist der Bitstatus {0, 1}, so ist der Aufruf möglicherweise ungültig. Bei einem möglicherweise ungültigen Aufruf wird die Funktion RtlpHandleInvalidUserCallTarget ausgeführt, um die angemessene Vorgehensweise zu bestimmen. Als Erstes prüft sie, ob in dem Prozess unterdrückte Aufrufe erlaubt sind. Diese Kompatibilitätsoption ist zwar ungewöhnlich, kann aber bei eingeschaltetem Application Verifier oder in der Registrierung festgelegt sein. Wenn das der Fall ist, wird geprüft, ob die Adresse unterdrückt wurde, was dann der Grund dafür wäre, dass sie nicht in die Bitmap eingefügt wurde. (Wie bereits erwähnt, wird dies durch ein besonderes Flag im Schutzfunktionstabelleneintrag angezeigt.) Wenn ja, wird der Aufruf zugelassen. Ist die Funktion dagegen überhaupt nicht gültig (also nicht in der Tabelle vorhanden), dann wird der Aufruf abgebrochen und der Prozess beendet. Zweitens wird geprüft, ob die Exportunterdrückung aktiviert ist. Wenn ja, wird die Zieladresse mit der Liste unterdrückter Adressen verglichen. Auch diese Unterdrückung wird durch Schutz gegen Exploits Kapitel 7 873
ein Flag im Schutzfunktionstabelleneintrag angezeigt. Wenn das der Fall ist, vergewissert sich der Lader, dass die Zieladresse ein Forwarder-Verweis auf die Exporttabelle einer anderen DLL ist, was nur bei einem indirekten Aufruf in einem Abbild mit unterdrückten Exporten zulässig ist. Das geschieht mithilfe eines komplizierten Prüfvorgangs, bei dem sichergestellt wird, dass sich die Zieladresse in einem anderen Abbild befindet, dass in dessen Abbildladeverzeichnis die Exportunterdrückung aktiviert wurde und dass die Adresse im Importverzeichnis dieses Abbilds steht. Wenn alle Überprüfungen bestanden sind, wird der Kernel mit der bereits beschriebenen Funktion NtSetInformationVirtualMemory aufgerufen, um den Bitstatus in {1, 0} zu ändern. Schlägt dagegen eine einzige dieser Überprüfungen fehl oder ist die Exportunterdrückung nicht aktiviert, wird der Prozess abgebrochen. Bei 32-Bit-Anwendungen erfolgt eine zusätzliche Prüfung, um zu sehen, ob die Datenausführungsverhinderung (DEP) für den Prozess aktiviert ist (siehe Kapitel 5). Wenn nicht, wird der inkorrekte Aufruf zugelassen, da dann keine Ausführungsgarantien vorhanden sind und es sich auch um eine ältere Anwendung handeln kann, die aus legitimen Gründen einen Aufruf auf dem Heap oder Stack vornimmt. Da aus Platzgründen große Mengen von {0, 0}-Bitstatus nicht zugesichert sind, tritt eine Ausnahme aufgrund einer Zugriffsverletzung auf, wenn die Überprüfung der CFG-Bitmap auf einer reservierten Seite landet. Auf x86-Systemen, auf denen die Einrichtung einer Ausnahmebehandlung mit großem Aufwand verbunden ist, wird diese Aufnahme daher nicht vom Verifizierungscode gehandhabt, sondern auf die übliche Weise weitergeleitet (siehe Kapitel 8 in Band 2). KiUserExceptionDispatcher, der Dispatcherhandler für den Benutzermodus, führt besondere Prüfungen durch, um in der Validierungsfunktion Ausnahmen aufgrund einer Zugriffsverletzung in Zusammenhang mit der CFG-Bitmap zu erkennen, und nimmt beim Ausnahmecode STATUS_IN_PAGE_ERROR die Ausführung automatisch wieder auf. Das macht den Code in LdrpValidateUserCallTarget(ES) und LdrpDispatchUserCallTarget(ES) einfacher, da diese Funktionen dann keinen Code zur Ausnahmebehandlung enthalten müssen. Auf x64-Systemen, auf denen Ausnahmehandler einfach in Tabellen registriert werden, wird stattdessen der Handler LdrpICallHandler mit der gleichen Logik wie zuvor ausgeführt. Kernel-CFG Die Binärdaten von Treibern, die in Visual Studio mit der Option /guard:cf kompiliert wurden, weisen die gleichen Eigenschaften auf wie Benutzermodusabbilder, doch die ersten Versionen von Windows 10 konnten mit diesen Daten nichts anfangen. Im Gegensatz zur CFG-Bitmap für den Benutzermodus, die von einer höheren, vertrauenswürdigeren Entität geschützt wird (dem Kernel), gibt es nichts, was eine Kernel-CFG-Bitmap schützen könnte, falls sie erstellt würde. Ein Exploit könnte einfach den PTE für die Seite mit den Bits, die er ändern möchte, als schreibbar kennzeichnen, und dann mit einem indirekten Aufruf oder Sprung fortfahren. Da sich die mögliche Abwehrmaßnahme so leicht umgehen ließ, hätte sich der Aufwand für ihre Einrichtung nicht gelohnt. Mit wachsender Anzahl der Benutzer, die VBS-Funktionen aktivieren, lässt sich jedoch wiederum die höhere Sicherheit nutzen, die VTL 1 bietet. Die SLAT-Seitentabelleneinträge bieten eine zweite Schutzvorkehrung gegen Änderungen am PTE-Seitenschutz. Da SLAT-Einträge als nur lesbar gekennzeichnet sind, ist die Bitmap in VTL 0 lesbar, doch wenn ein Kernelangreifer 874 Kapitel 7 Sicherheit
versucht, die PTEs zu ändern, um sie als schreibbar zu markieren, kann er das nicht auch mit den SLAT-Einträgen machen. Daher wird der Vorgang als ungültiger Zugriff auf die KCFG-Bitmap erkannt, bei dem HyperGuard eingreifen kann (nur aus Telemetriegründen, da die Bits ohnehin nicht geändert werden können). KCFG ist fast auf die gleiche Weise implementiert wie der reguläre Ablaufsteuerungsschutz, allerdings sind die Exportunterdrückung, die Unterstützung, longjmp und die Möglichkeit zur dynamischen Abfrage von zusätzlichen Bits für die JIT-Kompilierung nicht eingeschaltet, da der Kerneltreiber nichts davon tun sollte. Gesetzt werden die Bits in der Bitmap stattdessen anhand der Einträge in der Address-Taken-IAT, falls solche vorhanden sind, und anhand der üblichen Funktionseinträge in der Wächtertabelle, wenn ein Treiberabbild geladen wird. Für die HAL und den Kernel werden die Bits beim Start mithilfe von MiInitializeKernelCfg gesetzt. Ist der Hypervisor nicht aktiviert und keine SLAT-Unterstützung vorhanden, wird nichts davon initialisiert, weshalb Kernel-CFG ausgeschaltet bleibt. Wie im Benutzermodus wird auch hier ein dynamischer Zeiger im Ladekonfigurationsverzeichnis aktualisiert. Ist Kernel-CFG aktiviert, so verweist dieser Zeiger für die Überprüfungsfunktion auf __guard_check_icall und für die Dispatchfunktion im erweiterten CFG-Modus auf __guard_dispatch_icall. Darüber hinaus enthält die Variable guard_icall_bitmap die virtuelle Adresse der Bitmap. Leider sind die dynamischen Einstellungen der Treiberüberprüfung bei Kernel-CFG nicht konfigurierbar (mehr über die Treiberüberprüfung erfahren Sie in Kapitel 6), da es dazu erforderlich wäre, dynamische Kernelhooks hinzuzufügen und die Ausführung an Funktionen umzuleiten, die sich nicht unbedingt in der Bitmap befinden. In diesem Fall wird STATUS_VRF_CFG_­ ENABLED (0xC000049F) zurückgegeben, und es ist ein Neustart erforderlich (bei dem die Bitmap mit den Treiberverifizierungshooks erstellt werden kann). Zusicherungen Wir haben bereits beschrieben, dass der Ablaufsteuerungsschutz Prozesse beendet und dass auch andere Abwehrmaßnahmen und Sicherheitseinrichtungen Ausnahmen aufwerfen, um Prozesse abzubrechen. Es ist jedoch wichtig zu wissen, was genau dabei geschieht. Wenn eine solche Sicherheitsverletzung auftritt – wenn also beispielsweise der Ablaufsteuerungsschutz einen unzulässigen indirekten Aufruf oder Sprung erkennt –, wäre es nicht angebracht, den Prozess einfach mit dem Standardmechanismus TerminateProcess zu beenden, denn dabei ist weder ein Absturz garantiert noch werden Telemetriedaten an Microsoft gesendet. Beide Vorgänge sind aber wichtige Hilfsmittel: Sie helfen Administratoren, zu erkennen, dass möglicherweise ein Exploit aktiv war oder dass es ein Kompatibilitätsproblem gibt, und erlauben Microsoft, Zero-Day-Exploits in »freier Wildbahn« aufzuspüren. Es gibt jedoch auch einen Nachteil. Zwar erzielen Ausnahmen das gewünschte Ergebnis, doch es handelt sich bei ihnen um Callbacks, was die folgenden Konsequenzen haben kann: QQ Wenn die Abwehrmaßnahmen SAFESEH und SEHOP nicht aktiviert sind, können sich Angreifer darin einklinken und dafür sorgen, dass als Sicherheitsprüfung diejenige verwendet wird, über die Angreifer überhaupt erst die Kontrolle gegeben wird. Es ist auch möglich, dass der Angreifer die Ausnahme einfach unter den Tisch fallen lässt. Schutz gegen Exploits Kapitel 7 875
QQ Es ist möglich, dass sich legitime Teile der Software durch einen Filter für unbehandelte Ausnahmen oder einen vektorisierten Ausnahmehandler darin einklinken, was beides dazu führen kann, dass die Ausnahme versehentlich verschluckt wird. QQ Auch Drittanbieterprodukte, die ihre eigene Bibliothek in den Prozess injiziert haben, können die Ausnahme abfangen. Dies ist bei vielen Sicherheitswerkzeugen üblich und kann ebenfalls dazu führen, dass die Ausnahme nicht korrekt an die Windows-Fehlerbericht­ erstattung (Windows Error Reporting, WER) ausgeliefert wird. QQ Ein Prozess kann einen Callback zur Anwendungswiederherstellung bei der Windows-Fehlerberichterstattung registriert haben. Das kann zur Folge haben, dass dem Benutzer eine unklare Meldung angezeigt wird, weshalb er den Prozess in dem angegriffenen Zustand neu startet. Das kann zu allem Möglichen führen, von einer rekursiven Absturz/Start-Schleife bis zu einem vollständigen Verschlucken der Ausnahme. QQ In C++-Produkten kann die Ausnahme von einem äußeren Ausnahmehandler abgefangen werden, als wäre sie von dem Programm selbst aufgeworfen worden. Auch das kann dazu führen, dass die Ausnahme verschluckt oder die Ausführung auf unsichere Weise fortgesetzt wird. Um diese Probleme zu lösen, benötigen wir einen Mechanismus, der Ausnahmen so aufwirft, dass sie nicht von Komponenten des Prozesses außerhalb des WER-Dienstes abgefangen werden können. Des Weiteren muss garantiert sein, dass der WER-Dienst die Aufnahme auch empfängt. Dazu werden Zusicherungen verwendet. Unterstützung durch den Compiler und das Betriebssystem Wenn Bibliotheken, Programme oder Kernelkomponenten von Microsoft auf ungewöhnliche sicherheitsrelevante Situationen stoßen oder wenn Abwehrmaßnahmen gefährliche Verletzungen des Sicherheitsstatus erkennen, verwenden sie die besondere, von Visual Studio unterstützte systeminterne Funktion __fastfail, die einen Parameter als Eingabe entgegennimmt. Alternativ können sie die Laufzeitbibliotheksfunktion RtlFailFast2 in Ntdll aufrufen, die ihrerseits die systeminterne Funktion __fastfail nutzt. In manchen Fällen enthält das WDK oder das SDK Inline-Funktionen, die diese systeminterne Funktion aufrufen, z. B. bei der Verwendung der LIST_ENTRY-Funktionen InsertTailLists und RemoveEntryList. In anderen Situationen ist es die Universal CRT (uCRT) selbst, die in ihren Funktionen diese systeminterne Funktion enthält. Darüber hinaus gibt es auch Fälle, in denen APIs beim Aufruf durch Anwendungen bestimmte Überprüfungen vornehmen und dann ebenfalls die systeminterne Funktion einsetzen. Wenn der Compiler diese systeminterne Funktion erkennt, generiert er in jedem Fall Assemblercode, der den Eingabeparameter entgegennimmt, verschiebt ihn in das Register RCX (x64) oder ECX (x86) und gibt dann den Softwareinterrupt 0x29 aus. (Mehr über Interrupts erfahren Sie in Kapitel 8 von Band 2.) In Windows 8 und höher wird dieser Softwareinterrupt in der Interruptdispatchtabelle (IDT) mit dem Handler KiRaiseSecurityCheckFailure registriert, was Sie sich mit dem Debuggerbefehl !idt 29 selbst ansehen können. Aus Kompatibilitätsgründen führt dies dazu, dass KiFastFailDispatch mit dem Statuscode STATUS_STACK_BUFFER_OVERRUN (0xC0000409) aufgerufen wird. Diese Funktion führt eine reguläre Ausnahmedisponierung mit KiDispatchExcecption 876 Kapitel 7 Sicherheit
durch, behandelt den Vorfall aber als Ausnahme bei einem zweiten Versuch, weshalb der Debugger und der Prozess nicht benachrichtigt werden. Dieser Umstand wird erkannt, und es wird wie gewöhnlich eine Fehlermeldung an den ALPC-Fehlerport von WER gesendet. WER bezeichnet die Ausnahme als nicht fortsetzbar, was dazu führt, dass der Kernel den Prozess mit dem üblichen Systemaufruf ZwTerminateProcess beendet. Dadurch ist garantiert, dass innerhalb des Prozesses nach dem Auslösen des Interrupts keine Rückkehr zum Benutzermodus mehr erfolgt, dass WER benachrichtigt und der Prozess beendet wird. (Überdies ist der Fehlercode der Ausnahmecode.) Beim Erstellen des Ausnahmedatensatzes ist das erste Ausnahmeargument der Eingabeparameter für __fastfail. Auch Kernelmoduscode kann Ausnahmen aufwerfen, aber in diesem Fall wird KiBug­ CheckDispatch aufgerufen, was zu einem besonderen Kernelmoduscrash (»Bugcheck«) mit dem Code 0x129 führt (KERNEL_SECURITY_CHECK_FAILURE). Das erste Argument ist der Eingabeparameter für __fastfail. Fehlercodes Da das Eingabeargument von __fastfail an den Ausnahmedatensatz oder den Absturzbildschirm weitergeleitet wird, kann die Fehlerprüfung erkennen, welcher Teil des Systems oder des Prozesses nicht korrekt funktioniert oder eine Sicherheitsverletzung erlitten hat. Tabelle 7–22 zeigt die verschiedenen Fehlerbedingungen und ihre Bedeutung. Code Bedeutung Legacy OS Violation (0x0) Eine ältere Puffersicherheitsprüfung in einer veralteten ­Binärdatei ist fehlgeschlagen und wurde in eine Zusicherung umgewandelt. V-Table Guard Failure (0x1) Die Abwehrmaßnahme Virtual Table Guard in Internet ­Explorer 10 hat einen beschädigten Zeiger auf eine Tabelle für virtuelle Funktionen erkannt. Stack Cookie Check Failure (0x2) Das mit der Compileroption /GS generierte Stackcookie (auch Stackcanary genannt) wurde beschädigt. Corrupt List Entry (0x3) Eines der Makros für die Bearbeitung von LIST_ENTRY-Strukturen hat eine inkonsistente verknüpfte Liste entdeckt, in der ein Großeltern- oder Enkeleintrag nicht auf den Eltern- oder Kindeintrag des zu bearbeitenden Elements zeigt. Incorrect Stack (0x4) Eine Benutzer- oder Kernelmodus-API, die häufig von ROP-­ Exploits auf einem vom Angreifer kontrollierten Stack verwendet wird, wurde aufgerufen. Der Stack ist daher nicht der erwartete. Invalid Argument (0x5) Eine Benutzermodus-CRT-API (der übliche Fall) oder eine andere sensible Funktion wurde mit einem ungültigen Argument aufgerufen. Das deutet auf eine Verwendung durch einen ROP-Exploit oder einen aus anderen Gründen beschädigten Stack hin. Stack Cookie Init Failure (0x6) Die Initialisierung des Stackcookies ist fehlgeschlagen, was auf Abbildpatching oder eine Beschädigung hindeutet. → Schutz gegen Exploits Kapitel 7 877
Code Bedeutung Fatal App Exit (0x7) Die Anwendung hat die Benutzermodus-API FatalApp#Exit verwendet, die in eine Zusicherung umgewandelt wurde, um ihr die damit einhergehenden Vorteile zu gewähren. Range Check Failure (0x8) Zusätzliche Validierungsprüfungen in bestimmten festen Arraypuffern, um festzustellen, ob sich der Index des Array­ elements innerhalb der erwarteten Grenzen befindet Unsafe Registry Access (0x9) Ein Kernelmodustreiber versucht, von einem Hive aus, der vom Benutzer gesteuert werden kann (z. B. einem Anwendungs- oder Benutzerprofilhive), auf Registrierungsdaten zuzugreifen, ohne sich dabei mit dem Flag RTL_QUERY_­ REGISTRY_TYPECHECK zu schützen. CFG Indirect Call Failure (0xA) Der Ablaufsteuerungsschutz hat eine indirekte CALL- oder JMP-Anweisung zu einer Zieladresse erkannt, die laut CFG-­ Bitmap nicht gültig ist. CFG Write Check Failure (0xB) Der Ablaufsteuerungsschutz mit Schreibschutz hat einen ungültigen Schreibvorgang an geschützten Daten erkannt. Diese Option (/guard:cfw) steht nur bei Tests von Microsoft zur Verfügung. Invalid Fiber Switch (0xC) Die API SwitchToFiber wurde auf eine ungültige Fiber oder von einem Thread aus angewendet, der nicht in eine Fiber konvertiert wurde. Invalid Set of Context (0xD) Bei dem Versuch, eine Kontextdatensatzstruktur wiederherzustellen (aufgrund einer Ausnahme oder der API SetThreadContext), wurde erkannt, dass der Stackzeiger ungültig ist. Dies wird nur geprüft, wenn der Ablaufsteuerungsschutz für den Prozess eingeschaltet ist. Invalid Reference Count (0xE) Bei einem Objekt mit Referenzzählung (z. B. OBJECT_HEADER im Kernelmodus oder ein GDI-Objekt von Win32k.sys) ist der Referenzzähler unter 0 gefallen oder hat seinen Maximalwert überschritten und ist damit wieder auf 0 gegangen. Invalid Jump Buffer (0x12) Es wurde ein longjmp-Versuch mit einem Sprungpuffer versucht, der eine ungültige Stackadresse oder einen ungültigen Anweisungszeiger enthält. Dies wird nur geprüft, wenn der Ablaufsteuerungsschutz für den Prozess eingeschaltet ist. MRDATA Modified (0x13) Der änderbare, schreibgeschützte Datenheapabschnitt des Laders wurde geändert. Dies wird nur geprüft, wenn der Ablaufsteuerungsschutz für den Prozess eingeschaltet ist. Certification Failure (0x14) Eine oder mehrere Kryptografiedienst-APIs haben bei der Analyse eines Zertifikats oder eines ungültigen ASN.1-Streams ein Problem festgestellt. Invalid Exception Chain (0x15) Bei einem mit /SAFESEH oder SEHOP verlinkten Abbild ist ein ungültiger Ausnahmehandler disponiert worden. Crypto Library (0x16) Bei CNG.SYS, KSECDD.SYS oder ihren zugehörigen APIs im Benutzer ist ein kritischer Fehler aufgetreten. Invalid Call in DLL Callout (0x17) Es ist versucht worden, eine gefährliche Funktion im Benachrichtigungscallback des Benutzermodusladers aufzurufen. Invalid Image Base (0x18) Der Benutzermodus-Abbildlader hat einen ungültigen Wert für __ImageBase (IMAGE_DOS_HEADER-Struktur) erkannt. 878 Kapitel 7 Sicherheit →
Code Bedeutung Delay Load Protection Failure (0x19) Beim verzögerten Laden einer importierten Funktion wurde erkannt, dass die Delay-Load-IAT beschädigt ist. Dies wird nur geprüft, wenn der Ablaufsteuerungsschutz für den Prozess und der Schutz der Delay-Load-IAT eingeschaltet sind. Unsafe Extension Call (0x1A) Wird geprüft, wenn bestimmte Kernelmoduserweiterungs-APIs aufgerufen werden und der Status des Aufrufers nicht korrekt ist. Deprecated Service Called (0x1B) Wird geprüft, wenn nicht mehr unterstützte oder nicht dokumentierte Systemaufrufe verwendet werden. Invalid Buffer Access (0x1C) Wird von den Laufzeitbibliotheksfunktionen in Ntdll und dem Kernel geprüft, wenn eine allgemeine Pufferstruktur auf irgendeine Weise beschädigt ist. Invalid Balanced Tree (0x1D) Wird von den Laufzeitbibliotheksfunktionen in Ntdll und dem Kernel geprüft, wenn eine RTL_RB_TREE- oder RTL_AVL-­ TABLE-Struktur ungültige Knoten aufweist (wenn zwischen Geschwister- und/oder Elternknoten und den Großelternknoten Unstimmigkeiten bestehen; ähnlich wie bei den LIST_­ENTRYPrüfungen). Invalid Next Thread (0x1E) Wird vom Kernelscheduler geprüft, wenn der nächste einzuplanende Thread im KPRCB auf irgendeine Weise ungültig ist. CFG Call Suppressed (0x1F) Wird geprüft, wenn der Ablaufsteuerungsschutz einen unterdrückten Aufruf aus Kompatibilitätsgründen zulässt. In dieser Situation kennzeichnet WER den Fehler als behandelt, sodass der Kernel den Prozess nicht beendet. Die Telemetriedaten werden jedoch nach wie vor an Microsoft gesendet. APCs Disabled (0x20) Wird vom Kernel geprüft, wenn eine Rückkehr in den Benutzermodus erfolgt und die Kernel-APCs nach wie vor deaktiviert sind. Invalid Idle State (0x21) Wird von der Kernel-Energieverwaltung geprüft, wenn die CPU versucht, einen ungültigen C-Zustand einzunehmen. MRDATA Protection Failure (0x22) Wird vom Benutzermoduslader geprüft, wenn der Schutz des änderbaren, schreibgeschützten Heapabschnitts (Mutable Read-Only Heap Section) bereits außerhalb des erwarteten Codepfads aufgehoben wurde. Unexpected Heap Exception (0x23) Wird vom Heap-Manager geprüft, wenn der Heap auf eine Weise beschädigt ist, die auf einen möglichen Exploit hin­ deutet. Invalid Lock State (0x24) Wird vom Kernel geprüft, wenn bestimmte Sperren nicht den erwarteten Zustand aufweisen, z. B. wenn eine erworbene Sperre bereits freigegeben ist. Invalid Longjmp (0x25) Wird vom Aufruf von longjmp geprüft, wenn der Ablaufsteuerungsschutz mit Longjump-Schutz für den Prozess eingeschaltet ist, aber die Longjump-Tabelle beschädigt ist oder fehlt. Invalid Longjmp Target (0x26) Wie vor; in diesem Fall aber zeigt die Longjump-Tabelle an, dass die Longjump-Zielfunktion ungültig ist. Invalid Dispatch Context (0x27) Wird vom Ausnahmehandler im Kernelmodus geprüft, wenn eine Ausnahme mit einem falschen CONTEXT-Datensatz disponiert werden soll. → Schutz gegen Exploits Kapitel 7 879
Code Bedeutung Invalid Thread (0x28) Wird vom Scheduler im Kernelmodus geprüft, wenn die KTHREAD-Struktur bei bestimmten Zeitplanungsoperationen beschädigt ist. Invalid System Call Number (0x29) Ähnelt Deprecated Service Called; hier allerdings kennzeichnet WER die Ausnahme als behandelt, sodass der Prozess fortgesetzt und die Ausnahme nur für die Telemetrie verwendet wird. Invalid File Operation (0x2A) Wird vom E/A-Manager und bestimmten Dateisystemen als eine weitere Form von Fehler für die Telemetrie behandelt (wie beim vorherigen Code). LPAC Access Denied (0x2B) Wird von der Zugriffsprüfungsfunktion des SRM verwendet, wenn ein Anwendungscontainer mit niedrigen Rechten versucht, auf ein Objekt zuzugreifen, für das die SID ALL ­RESTRICTED APPLICATION PACKAGES nicht gesetzt ist, und die Verfolgung solcher Fehler aktiviert ist. Auch hier werden die Ergebnisse nur für Telemetriedaten verwendet und führen nicht zum Absturz des Prozesses. RFG Stack Failure (0x2C) Wird von RFG (Return Flow Guard) verwendet, allerdings ist dieses Merkmal zurzeit deaktiviert. Loader Continuity Failure (0x2D) Wird von der zuvor gezeigten gleichnamigen Abwehrmaßnahme verwendet, um anzuzeigen, dass ein unerwartetes Abbild mit einer anderen Signatur oder gar keiner Signatur geladen wurde. CFG Export Suppression Failure (0x2D) Wird vom Ablaufsteuerungsschutz mit Exportunterdrückung verwendet, um anzuzeigen, dass ein unterdrückter Export das Ziel einer indirekten Verzweigung war. Invalid Control Stack (0x2E) Wird von RFG (Return Flow Guard) verwendet, allerdings ist dieses Merkmal zurzeit deaktiviert. Set Context Denied (0x2F) Wird von der zuvor gezeigten gleichnamigen Abwehrmaßnahme verwendet, allerdings ist dieses Merkmal zurzeit deaktiviert. __fastfail-Fehlercodes Tabelle 7–22  Anwendungsidentifizierung In Windows wurden Sicherheitsentscheidungen traditionell auf der Grundlage der Identität des Benutzers (in Form seiner SID und seiner Gruppenmitgliedschaften) gefällt, aber mehr und mehr Sicherheitskomponenten (AppLocker, Firewall, Antivirus- und Antimalwareprogramme, Rights Management Services usw.) müssen ihre sicherheitsrelevanten Entscheidungen darauf gründen, was für Code ausgeführt wird. Früher verwendeten all diese Komponenten jeweils ihre eigenen Methoden, um Anwendungen zu identifizieren, weshalb das Schreiben von Richtlinien sehr inkonsistent und übermäßig kompliziert war. Die Anwendungsidentifizierung mithilfe einer AppID soll die Erkennung von Anwendungen durch Sicherheitskomponenten vereinheitlichen, indem sie einen einzigen Satz von APIs und Datenstrukturen für diesen Zweck bereitstellt. 880 Kapitel 7 Sicherheit
Dies ist etwas anderes als die AppID, die von DCOM/COM+-Anwendungen eingesetzt wird und bei der ein GUID für einen Prozess steht, der von mehreren CLSIDs gemeinsam genutzt wird. Es hat auch nichts mit der UWP-Anwendungs-ID zu tun. Unmittelbar bevor eine Anwendung startet, wird sie identifiziert. Dazu wird die AppID des Hauptprogramms generiert. Dazu können beliebige der folgenden Attribute der Anwendung herangezogen werden: QQ Felder Mithilfe der Felder in dem Zertifikat zur Codesignierung, das in die Datei eingebettet ist, können verschiedene Kombinationen von Herausgeber-, Produkt- und Dateiname sowie Version verwendet werden. APPID://FQBN ist ein vollständig qualifizierter Binärdateienname. Dabei handelt es sich um einen String der Form {Herausgeber/Produkt/ Dateiname,Version}. Publisher ist das Feld Subject des X.509-Zertifikats, das zum Signieren des Codes verwendet wurde, wobei folgende Felder berücksichtigt werden: • O Organisation • L Ort (Locality) • S Bundesstaat, Bundesland, Provinz o. Ä. (State) • C Land (Country) QQ Dateihash Für das Hashing können verschiedene Methoden eingesetzt werden, wobei APPID://SHA256HASH die Standardmethode ist, bei der ein SHA-256-Hash der Datei ange- legt wird. Zur Abwärtskompatibilität mit SRP und den meisten X.509-Zertifikaten wird jedoch auch SHA-1 unterstützt (APPID://SHA1HASH). QQ Teilpfad oder vollständiger Pfad zu der Datei APPID://Path gibt einen Pfad an, in dem optional auch Jokerzeichen (*) vorkommen können. Mit einer AppID wird auf keinen Fall die Qualität oder Sicherheit einer Anwendung bestätigt. Sie dient lediglich dazu, die Anwendung zu identifizieren, sodass Administratoren bei sicherheitsrelevanten Entscheidungen auf die Anwendung verweisen können. Die AppID wird im Prozesszugriffstoken gespeichert, sodass Sicherheitskomponenten Autorisierungsentscheidungen auf der Grundlage einer einzigen, einheitlichen Identifizierung treffen können. AppLocker verwenden bedingte Zugriffssteuerungseinträge, um anzugeben, ob der Benutzer ein bestimmtes Programm ausführen darf oder nicht. Beim Erstellen einer AppID für eine signierte Datei wird das Zertifikat der Datei zwischengespeichert und zu einem vertrauenswürdigen Stammzertifikat zurückverfolgt und überprüft. Der Zertifikatpfad wird täglich erneut überprüft, um sicherzugehen, dass er nach wie vor gültig ist. Diese Zwischenspeicherung und Verifizierung wird im Systemereignisprotokoll unter Application und Services Logs\Microsoft\Windows\AppID\Operational aufgezeichnet. Anwendungsidentifizierung Kapitel 7 881
AppLocker In den Enterprise-Editionen von Windows 8.1 und Windows 10 sowie in Windows Server 2012/ R2/2016 gibt es das Feature AppLocker, mit dem Administratoren ein System sperren können, um die Ausführung nicht autorisierter Programme zu verhindern. In Windows XP wurden die Richtlinien für Softwareeinschränkung (Software Restriction Policies, SRP) eingeführt, die einen ersten Schritt in diese Richtung darstellte, aber sich nur schwer verwalten ließ und nicht auf einzelne Benutzer und Gruppen angewendet werden konnte. (Die SRP-Regeln wirkten sich auf alle Benutzer aus.) AppLocker ersetzt SRP, kann aber auch parallel dazu verwendet werden, da die AppLocker-Regeln getrennt von den SRP-Regeln gespeichert werden. Befinden sich in einem Gruppenrichtlinienobjekt sowohl AppLocker- als auch SRP-Regeln, so werden nur die AppLocker-Regeln angewendet. Ein weiteres Merkmal, das AppLocker gegenüber SRP überlegen macht, ist der Überwachungsmodus, in dem ein Administrator eine AppLocker-Richtlinie erstellen und die (im System­ereignisprotokoll gespeicherten) Ergebnisse untersuchen kann, um festzustellen, ob sich die Richtlinie wie erwartet verhält, ohne die Einschränkungen tatsächlich anzuwenden. Mit dem AppLocker-Überwachungsmodus lassen sich auch Anwendungen beobachten, die von einem oder mehreren Benutzern auf dem System genutzt werden. Mit AppLocker können Administratoren die Ausführung folgender Arten von Dateien einschränken: QQ Ausführbare Abbilder (EXE und COM) QQ Dynamische Programmbibliotheken (DLL und OCX) QQ Microsoft Software Installer (MSI und MSP) zum Installieren und Deinstallieren QQ Skripte QQ Windows PowerShell (PS1) QQ Batchdateien (BAT und CMD) QQ VisualBasic Script (VBS) QQ JavaScript (JS) Um festzulegen, welche Anwendungen und Skripte von einzelnen Benutzern und Gruppen ausgeführt werden dürfen, verwendet AppLocker einen einfachen regelbasierten Mechanismus mit einer grafischen Oberfläche, der dem Mechanismus für Firewallregeln sehr stark ähnelt. Dabei werden bedingte Zugriffssteuerungseinträge und AppID-Attribute verwendet. In AppLocker gibt es die beiden folgenden Arten von Regeln: QQ Regeln, die die Ausführung der angegebenen Dateien zulassen und alles andere verweigern QQ Regeln, die die Ausführung der angegebenen Dateien verweigern und alles andere zulassen. Verweigerungsregeln haben Vorrang vor Zulassungsregeln. Zu einer Regel kann auch eine Liste von Dateien gehören, die von der Regel ausgenommen sind. Dadurch können Sie z. B. eine Regel erstellen, nach der alles, was sich in C:\Windows oder C:\Programme befindet, ausgeführt werden darf, nur RegEdit.exe nicht. 882 Kapitel 7 Sicherheit
AppLocker-Regeln können mit einzelnen Benutzern und Gruppen verknüpft werden. Dadurch können Administratoren Compliance-Anforderungen umsetzen, indem sie festlegen, welche Benutzer bestimmte Anwendungen ausführen dürfen. Beispielsweise können Sie eine Regel aufstellen, die den Benutzern in der Sicherheitsgruppe Finanzen die Ausführung von Finanzanwendungen erlaubt. Dadurch werden alle, die sich nicht in dieser Gruppe befinden, davon abgehalten, diese Anwendungen zu nutzen (auch Administratoren), während diejenigen, die diese Anwendungen für ihre geschäftlichen Zwecke benötigen, darauf zugreifen können. Eine weitere nützliche Regel wäre beispielsweise eine, die verhindert, dass Benutzer in der Gruppe Empfang nicht genehmigte Software installieren oder ausführen. AppLocker-Regeln stützen sich auf bedingte Zugriffssteuerungsregeln und AppID-Attribute. Sie können unter Verwendung der folgenden Kriterien aufgestellt werden: QQ Felder in dem Zertifikat zur Codesignierung, das in die Datei eingebettet ist, wobei verschiedene Kombinationen von Herausgeber-, Produkt- und Dateiname sowie Version verwendet werden können Beispielsweise können Sie eine Regel erstellen, die die Ausführung aller Versionen von Contoso Reader größer als 9.0 zulässt oder allen Mitgliedern der Gruppe Grafik erlaubt, den Installer oder die Anwendung von Contoso für GraphicsShop auszuführen, sofern es sich um Version 14.* handelt. Der SDDL-String im folgenden Beispiel verweigert dem Benutzerkonto RestrictedUser (identifiziert anhand der Benutzer-SID) den Ausführungszugriff auf signierte Programme, die von Contoso herausgegeben wurden: D:(XD;;FX;;;S-1-5-21-3392373855-1129761602-2459801163-1028;((Exists APPID://FQBN) && ((APPID://FQBN) >= ({"O=CONTOSO, INCORPORATED, L=REDMOND, S=CWASHINGTON, C=US\*\*",0})))) QQ Verzeichnispfad, wobei nur Dateien in einem bestimmten Verzeichnisbaum ausgeführt werden dürfen Hiermit können auch einzelne Dateien bezeichnet werden. Der folgende SDDL-Beispielstring verweigert dem Benutzerkonto RestrictedUser (identifiziert anhand seiner SID) den Ausführungszugriff auf die Programme im Verzeichnis C:\Tools: D:(XD;;FX;;;S-1-5-21-3392373855-1129761602-2459801163-1028;(APPID://PATH Contains "%OSDRIVE%\TOOLS\*")) QQ Dateihash Bei der Verwendung eines Hashs lässt sich auch erkennen, ob eine Datei manipuliert wurde, und ihre Ausführung in einem solchen Fall verweigern. Dies kann jedoch von Nachteil sein, wenn die Dateien häufig geändert werden, da die Hashregel dann ebenso häufig angepasst werden muss. Dateihashes werden vor allem für Skripts verwendet, da diese nur selten signiert werden. Der folgende SDDL-Beispielstring verweigert dem Benutzerkonto RestrictedUser (identifiziert anhand seiner SID) den Ausführungszugriff auf die Programme mit den angegebenen Hashwerten: D:(XD;;FX;;;S-1-5-21-3392373855-1129761602-2459801163-1028;(APPID://SHA256HASH Any_of {#7a334d2b99d48448eedd308df​ca63b8a3b7b44044496ee2f8e236f5997f1b647, #2a782f76cb94ece307dc​52c338f02edbbfdca83906674e35c682724a8a92a76b})) AppLocker Kapitel 7 883
AppLocker-Richtlinien können auf dem lokalen Computer mithilfe des MMC-Snap-Ins Lokale Sicherheitsrichtlinie (Secpol.msc, siehe Abb. 7–33) oder einem Windows PowerShell-Skript erstellt werden. Auf Computern in einer Domäne können sie mithilfe einer Gruppenlinie bereitgestellt werden. AppLocker-Regeln werden an verschiedenen Stellen in der Registrierung gespeichert: QQ HKLM\Software\Policies\Microsoft\Windows\SrpV2 Dieser Schlüssel wird auch in HKLM\ SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\SrpV2 gespiegelt. Die Regeln werden im XML-Format gespeichert. QQ HKLM\SYSTEM\CurrentControlSet\Control\Srp\Gp\Exe Die Regeln werden im SDDL-­ Format und als binäre Zugriffssteuerungseinträge gespeichert. QQ HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Group Policy Objects\ {GUID}Machine\Software\Policies\Microsoft\Windows\SrpV2 Als Teil eines Gruppenrichtlinienobjekts in einer Domäne bereitgestellte AppLocker-Richtlinien werden hier im XML-Format gespeichert. Zertifikate für ausgeführte Dateien werden in der Registrierung unter HKLM\SYSTEM\CurrentControlSet\Control\AppID\CertStore gespeichert. AppLocker erstellt von dem Zertifikat in einer Datei eine Zertifikatkette zurück zu einem vertrauenswürdigen Stammzertifikat (die dann in HKLM\SYSTEM\CurrentControlSet\Control\AppID\CertChainStore gespeichert wird). Für skriptgestützte Entwicklung und Tests gibt es die AppLocker-spezifischen Power­ Shell-Cmdlets Get-AppLockerFileInformation, Get-AppLockerPolicy, New-AppLockerPolicy, Set-­ AppLockerPolicy und Test-AppLockerPolicy, die Sie mit Import-Module AppLocker in die Power­ Shell importieren können. Abbildung 7–33 AppLocker-Konfigurationsseite in der lokalen Sicherheitsrichtlinie 884 Kapitel 7 Sicherheit
Der AppLocker- und der SRP-Dienst befinden sich nebeneinander in derselben Binärdatei (AppIdSvc.dll), die in einem Diensthostprozess ausgeführt wird. Der Dienst fordert eine Benachrichtigung über Registrierungsänderungen an, um jegliche Änderungen an dem Schlüssel zu beobachten, die von einem Gruppenrichtlinienobjekt oder von dem AppLocker-Element im MMC-Snap-In Lokale Sicherheitsrichtlinie vorgenommen werden. Wird eine Änderung erkannt, so löst der AppID-Dienst den Benutzermodustask AppIdPolicyConverter.exe aus, um die neuen (in XML geschriebenen) Regeln zu lesen und in binäre Zugriffssteuerungseinträge und SDDL-Strings umzuwandeln, die sowohl für die Anwendungsidentifizierung im Benutzer- und Kernelmodus als auch für die AppLocker-Komponenten verständlich sind. Diese übersetzten Regeln speichert der Task im Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\Srp\Gp, der nur vom System und von Administratoren geschrieben werden kann und für authentifizierte Benutzer als lesbar, aber nicht schreibbar gekennzeichnet ist. Sowohl die Benutzer- als auch die Kernelmoduskomponenten der Anwendungsidentifizierung lesen die übersetzten Regeln direkt in der Registrierung. Der Dienst überwacht auch den Speicher für vertrauenswürdige Stammzertifikate auf dem lokalen Computer und ruft den Benutzermodustask AppIdCert­ StoreCheck.exe auf, um die Zertifikate mindestens einmal täglich und bei jeder Änderung am Zertifikatspeicher erneut zu überprüfen. Der Kernelmodustreiber für die Anwendungsidentifizierung (%SystemRoot%\System32\Drivers\AppId.sys) wird vom AppID-Dienst mithilfe der DeviceIoControl-Anforderung APPID_POLICY_CHANGED über Regeländerungen benachrichtigt. Nachdem AppLocker eingerichtet und der Dienst gestartet wurde, können Administratoren anhand des Systemereignisprotokolls in der Ereignisanzeige verfolgen, welche Anwendungen zugelassen oder verweigert werden (siehe Abb. 7–34). Abbildung 7–34 Die Ereignisanzeige zeigt, wie AppLocker den Zugriff auf verschiedene Anwen­ dungen zulässt oder verweigert. Das Ereignis 8004 zeigt eine Verweigerung, Ereignis 8002 eine Zulassung. AppLocker Kapitel 7 885
Die Implementierungen von Anwendungsidentifizierung, AppLocker und SRP gehen ineinander über. Da die verschiedenen logischen Komponenten nebeneinander in denselben ausführbaren Dateien vorhanden sind, gibt es auch keine strenge Schichtung. Auch die Benennung ist nicht so einheitlich, wie man es sich gern wünschen würde. Der AppID-Dienst wird als lokaler Dienst ausgeführt, weshalb er Zugriff auf den Speicher für vertrauenswürdige Stammzertifikate auf dem System hat. Dadurch kann er auch eine Verifizierung der Zertifikate durchführen. Zuständig ist dieser Dienst für folgende Aufgaben: QQ Verifizierung der Herausgeberzertifikate QQ Hinzufügen neuer Zertifikate zum Cache QQ Erkennen von Änderungen an AppLocker-Regeln und Benachrichtigen des AppID-Treibers Der AppID-Treiber kümmert sich um den Großteil der AppLocker-Funktionalität und stützt sich dabei auf die Kommunikation mit dem AppID-Dienst (über DeviceIoControl-Anforderungen). Sein Geräteobjekt ist mit einer Zugriffssteuerungsliste geschützt und gewährt den Zugriff nur für NT-DIENST\AppldSvc, LOKALER DIENST und BUILTIN\Administratoren. Daher kann dieser Treiber von Malware nicht gefälscht werden. Wenn der AppID-Treiber geladen wird, ruft er PsSetCreateProcessNotifyRoutineEx auf, um einen Prozesserstellungscallback anzufordern. Bei ihrem Aufruf wird der Benachrichtigungsroutine eine PPS_CREATE_NOTIFY_INFO-Struktur übergeben (die den bereits erstellten Prozess beschreibt). Anschließend erfasst die Routine die AppID-Attribute, die das ausführbare Abbild identifizieren, und schreibt sie in das Zugriffstoken des Prozesses. Danach ruft sie die nicht dokumentierte Routine SeSrpAccessCheck auf, die das Prozesstoken und die AppLocker-Regeln in Form von bedingten Zugriffssteuerungseinträgen untersucht und darüber bestimmt, ob der Prozess ausgeführt werden darf. Ist die Ausführung des Prozesses nicht erlaubt, schreibt der Treiber STATUS_ACCESS_DISABLED_BY_POLICY in das Feld Status der PPS_CREATE_NOTIFY_­INFOStruktur, was dazu führt, dass die Prozesserstellung abgebrochen wird, und den endgültigen Beendigungsstatus des Prozesses festlegt. Für die DLL-Einschränkung sendet der Abbildlader eine DeviceIoControl-Anforderung an den AppID-Treiber, wenn er eine DLL in einen Prozess lädt. Der Treiber prüft dann wie bei einer ausführbaren Datei die Identität der DLL anhand der bedingten AppLocker-Zugriffssteuerungseinträge. Die Durchführung dieser Überprüfungen für jede geladene DLL ist zeitraubend und kann eine für die Endbenutzer merkliche Verzögerung nach sich ziehen. Daher sind DLL-Regeln normalerweise deaktiviert und müssen eigens aktiviert werden. Dies geschieht auf der Registerkarte Erweitert auf der Eigenschaftenseite für App­ Locker im Snap-In Lokale Sicherheitsrichtlinie. Die Skript-Engines und der MSI-Installer wurden geändert, sodass sie beim Öffnen einer Datei die Benutzermodus-SRP-APIs aufrufen, um zu prüfen, ob die Datei überhaupt geöffnet werden darf. Diese APIs rufen die AuthZ-APIs auf, um die Zugriffsprüfung anhand der bedingten Zugriffssteuerungseinträge durchzuführen. 886 Kapitel 7 Sicherheit
Richtlinien für Softwareeinschränkung Windows enthält den Benutzermodusmechanismus der Richtlinien für Softwareeinschränkung (Software Restriction Policies, SRP), mit dem Administratoren festlegen können, welche Abbilder und Skripte auf ihren Systemen ausgeführt werden können. Der Knoten Richtlinien für Softwareeinschränkung im Editor für lokale Sicherheitsrichtlinien (siehe Abb. 7–35) dient als Verwaltungsschnittstelle für die Codeausführungsrichtlinien des Computers. Allerdings können benutzerspezifische Richtlinien auch über Gruppenrichtlinien in der Domäne festgelegt werden. Abbildung 7–35 Konfiguration der Richtlinien für Softwareeinschränkung Unter dem Knoten Richtlinien für Softwareeinschränkung befinden sich verschiedene globale Richtlinieneinstellungen: QQ Erzwingen Diese Richtlinie legt fest, ob Einschränkungsrichtlinien auf Bibliotheken angewendet werden sollen, z. B. auf DLLs, und ob sie nur für Benutzer oder auch für Administratoren gelten. QQ Designierte Dateitypen Diese Richtlinie gibt an, in welchem Ausmaß Dateien als ausführbarer Code angesehen werden. QQ Vertrauenswürdige Herausgeber Diese Richtlinie bestimmt, wer auswählen kann, welchen Zertifikatherausgebern vertraut wird. Richtlinien für Softwareeinschränkung Kapitel 7 887
Bei der Konfiguration einer Richtlinie für ein bestimmtes Skript oder Abbild kann ein Administrator angeben, ob das Skript oder Abbild anhand seines Pfades, seines Hashs, seiner Internetzone (laut Definition in Internet Explorer) oder seines kryptografischen Zertifikats erkannt werden soll und ob es mit einer Sicherheitsrichtlinie vom Typ Nicht erlaubt oder Nicht eingeschränkt verknüpft wird. SRPs werden in den folgenden Komponenten erzwungen, die Dateien als ausführbaren Code behandeln: QQ Die Benutzermodusfunktion CreateProcess in Kernel32.dll erzwingt sie für ausführbare Abbilder. QQ Der Code zum Laden von DLLs in Ntdll erzwingt sie für DLLs. QQ Die Windows-Eingabeaufforderung (Cmd.exe) erzwingt sie für die Ausführung von Batchdateien. QQ Windows Scripting Host-Komponenten, die Skripte starten – Cscript.exe für Befehlszeilenskripte, Wscript.exe für Benutzerschnittstellenskripte und Scrobj.dll für Skriptobjekte –, erzwingen sie für die Skriptausführung. QQ Der PowerShell-Host (PowerShell.exe) erzwingt sie für die Ausführung von Power­ShellSkripten. Jede dieser Komponenten bestimmt, ob die Einschränkungsrichtlinien aktiviert sind, indem sie den Registrierungswert TransparentEnabled im Schlüssel HKLM\Software\Policies\Microsoft\ Windows\Safer\CodeIndentifiers liest. Wenn er auf 1 gesetzt ist, sind die Richtlinien in Kraft. Anschließend ermittelt die Komponente, ob auf den auszuführenden Code eine der Regeln zutrifft, die in einem Unterschlüssel von CodeIdentifiers angegeben sind. Wenn ja, prüft sie, ob die Ausführung nach dieser Regel erlaubt ist. Trifft dagegen keine Regel zu, wird anhand der Standard­ richtlinie im Wert DefaultLevel von CodeIdentifiers bestimmt, ob die Ausführung zulässig ist. Richtlinien für Softwareeinschränkung sind ein wirkungsvolles Werkzeug, um den nicht autorisierten Zugriff auf Code und Skripte zu verhindern, aber nur dann, wenn sie auch korrekt angewandt werden. Schon durch kleine Änderungen an einem als unzulässig gekennzeichneten Abbild kann die konkrete Regel dafür umgangen werden. Beispielsweise könnte ein Benutzer ein harmloses Byte eines Prozessabbilds bearbeiten, sodass es von der Hashregel nicht mehr erkannt wird, oder eine Datei an einen anderen Speicherort kopieren, um der Identifizierung anhand des Pfads auszuweichen. Wenn die Standardrichtlinie die Ausführung nicht verbietet, kann das Abbild dann ausgeführt werden. 888 Kapitel 7 Sicherheit
Experiment: Die Erzwingung von Richtlinien für Softwareeinschränkung beobachten Die Durchsetzung von SRPs können Sie indirekt beobachten, indem Sie während des Versuchs, ein unzulässiges Abbild auszuführen, auf die Registrierung zugreifen. 1. Führen Sie Secpol.msc aus, um den Editor für lokale Sicherheitsrichtlinien zu starten, und öffnen Sie den Knoten Richtlinien für Softwareeinschränkung. 2. Wenn keine Richtlinien definiert sind, wählen Sie im Kontextmenü Neue Richtlinien für Softwareeinschränkung erstellen. 3. Erstellen Sie unter dem Knoten Zusätzliche Regeln eine Pfadregel der Sicherheitsstufe Nicht erlaubt für %SystemRoot%\System32\Notepad.exe. 4. Starten Sie Process Monitor und richten Sie einen Pfadfilter für Safer ein. 5. Öffnen Sie eine Eingabeaufforderung und führen Sie dort den Editor (Notepad) aus. Bei dem Versuch, den Editor zu starten, erhalten Sie die Meldung, dass Sie das angegebene Programm nicht ausführen können. In Process Monitor können Sie beobachten, wie die Eingabeaufforderung (Cmd.exe) die lokalen Einschränkungsrichtlinien auf dem Computer abfragt. Kernelpatchschutz Manche Geräte ändern das Verhalten von Windows auf nicht vorgesehene Weise. Beispielsweise manipulieren sie die Systemaufruftabelle, um Systemaufrufe abzufangen, oder das Kernelabbild im Arbeitsspeicher, um den Funktionsumfang einzelner interner Funktionen zu erweitern. Solche Änderungen sind gefährlich und können die Stabilität und Sicherheit des Systems beeinträchtigen. Außerdem ist es möglich, dass solche Manipulationen in böser Absicht erfolgen, entweder durch schädliche Treiber oder durch Exploits, die Schwachstellen in Windows-Treibern ausnutzen. Da es keine Entität mit höheren Rechten gibt als den Kernel selbst, ist es ziemlich knifflig, Kernelexploits und schädliche Kerneltreiber zu erkennen und abzuwehren. Da sich sowohl die Erkennungs- und Schutzmechanismen als auch das unerwünschte Verhalten in Ring 0 bewegen, ist es nicht möglich, eine Sicherheitsgrenze im wahren Sinne des Wortes zu ziehen, da das unerwünschte Verhalten den Erkennungs- und Schutzmechanismus deaktivieren, manipulieren oder täuschen kann. Allerdings kann ein Mechanismus, der auf unerwünschten Operationen dieser Art reagiert, selbst unter diesen Umständen noch nützlich sein: QQ Wenn dieser Mechanismus den Computer mit einem eindeutigen Kernelmodus-Absturzabbild abstürzen lässt, können sowohl Benutzer als auch Administratoren leicht erkennen, dass im Kernel unerwünschtes Verhalten aufgetreten ist, und entsprechende Maßnahmen ergreifen. Da die Hersteller legitimer Software die Systeme ihrer Kunden nicht zum Absturz bringen wollen, werden sie eher geneigt sein, nach ordnungsgemäßen Wegen zu Kernelpatchschutz Kapitel 7 889
suchen, auf denen sie den Funktionsumfang des Kernels erweitern können (z. B. durch Verwendung des Filter-Managers für Dateisystemfilter oder durch andere Callbackmechanismen). QQ Verschleierung (Obfuscation) bildet zwar keine Sicherheitsgrenze, kann es aber für Schadcode zeitraubender oder komplizierter machen, den Erkennungsmechanismus auszuschalten. Die verlängerte Dauer bzw. die höhere Komplexität bewirken, dass das unerwünschte Verhalten deutlicher als potenziell schädlich erkannt werden kann, und bereiten dem Angreifer einen viel höheren Aufwand. Ein ständiger Wechsel der Verschleierungstechniken veranlasst Hersteller legitimer Software dazu, ihre veralteten Erweiterungsmechanismen aufzugeben und stattdessen unterstützte Techniken einzusetzen, bei denen nicht die Gefahr besteht, für Malware gehalten zu werden. QQ Randomisierung, fehlende Dokumentation der Prüfungen, die der Erkennungs- und Schutzmechanismus konkret durchführt, und Indeterminismus bei der Auswahl der Zeitpunkte, an denen die Prüfungen stattfinden, machen Exploits unzuverlässig. Angreifer sind gezwungen, alle möglichen indeterministischen Variablen und Zustandsübergänge des Mechanismus durch statische Analyse zu berücksichtigen. Dank der Verschleierung lässt sich eine solche Analyse aber unmöglich erledigen, bevor in dem Mechanismus der nächste Wechsel der Verschleierungstechnik oder eine Änderung von Merkmalen erfolgt. QQ Da Kernelmodus-Absturzabbilder automatisch an Microsoft übertragen werden, erhält das Unternehmen reale Telemetriedaten über unerwünschten Code. Damit kann es Softwarehersteller erkennen, die nicht unterstützten Code verwenden und damit für Systemabstürze sorgen, die Ausbreitung von Malware verfolgen, Zero-Day-Kernelexploits aufspüren und Bugs korrigieren, die niemals gemeldet wurden, aber von Angreifern ausgenutzt werden. PatchGuard Kurz nach der Veröffentlichung von 64-Bit-Windows für x64 und noch bevor sich ein umfassender Drittanbietermarkt entwickeln konnte, hat Microsoft die Gelegenheit ergriffen, die Stabilität von 64-Bit-Windows zu wahren. Dazu hat das Unternehmen mithilfe des Kernelpatchschutzes (Kernel Patch Protection, KPP), auch PatchGuard genannt, die Übermittlung von Telemetriedaten und eine Patcherkennung eingeführt, die Exploits lahmlegen. Als Windows Mobile veröffentlich wurde, das auf einem 32-Bit-ARM-Prozessorkern läuft, wurde dieses Feature auch auf solche Systeme portiert, und es wird auch auf 64-Bit-ARM-Systemem (AArch64) zur Verfügung stehen. Da es noch viel zu viele veraltete 32-Bit-Treiber gibt, die nicht unterstützte und gefährliche Hooktechniken verwenden, ist dieser Mechanismus auf den entsprechenden Systemen jedoch nicht aktiviert, nicht einmal unter Windows 10. Zum Glück geht die Ära der 32-Bit-­ Systeme langsam zu Ende. Die Serverversionen von Windows gibt es für die Architektur schon gar nicht mehr. Die Bezeichnungen Protection und Guard wecken zwar die Vorstellung, dass dieser Mechanismus das System schützt, allerdings besteht dieser Schutz nur darin, den Computer zum 890 Kapitel 7 Sicherheit
Absturz zu bringen, damit der unerwünschte Code nicht weiter ausgeführt werden kann. Der Mechanismus verhindert den Angriff nicht und macht ihn auch nicht rückgängig. Sie können sich den Kernelpatchschutz wie eine mit dem Internet verbundene Überwachungskamera vorstellen, die einen lauten Alarm (Absturz) im Banksafe (Kernel) auslöst, aber nicht als ein Sicherheitsschloss an dem Tresor. Der Kernelpatchschutz führt eine Vielzahl von verschiedenen Prüfungen durch. Sie alle zu dokumentieren, wäre nicht nur impraktikabel (aufgrund der Schwierigkeiten der statischen Analyse), sondern würde auch Angreifern entgegenspielen (da es die erforderliche Recherchezeit für sie verringern würde). Einige dieser Prüfungen sind jedoch von Microsoft dokumentiert. In Tabelle 7–23 finden Sie einen allgemein gehaltenen Überblick darüber. Wann, wo und wie der Kernelpatchschutz diese Überprüfungen durchführt und welche Funktionen und Daten­ strukturen dabei konkret betroffen sind, können wir hier jedoch nicht zeigen. Komponente Legitime Nutzung Unerwünschte Nutzung Ausführbarer Code im Kernel, seine Abhängigkeiten und Kerntreiber sowie die Importadresstabelle (IAT) dieser Komponenten Windows-Standardkomponenten, die für die Verwendung des Kernels von entscheidender Bedeutung sind Die Manipulation des Codes dieser Komponenten kann ihr Verhalten ändern, Hintertüren in dem System einrichten, Daten und unerwünschte Kommunikation verbergen, die Stabilität des Systems beeinträchtigen und aufgrund von Bugs in dem Drittanbietercode sogar zusätzliche Schwachstellen einbringen. Globale Deskriptortabelle (GDT) CPU-Hardwareschutz zur Implementierung von Ringen mit unterschiedlicher Rechteebene (Ring 0/Ring 3) Änderung der Berechtigungen und der Zuordnung zwischen Code und Ringebenen, sodass Ring-3-Code Zugriff auf Ring 0 bekommt Interruptdeskriptortabelle (IDT) oder Interruptvektortabelle Lesen in der Tabelle durch die CPU zur Auslieferung von Interruptvektoren an die zugehörigen Routinen zur Handhabung der Interrupts Abfangen von Tastenbetätigungen, Netzwerkpaketen, Systemaufrufen, des Auslagerungsmechanismus, der Hypervisorkommunikation usw. Das kann genutzt werden, um Hintertüren einzurichten und um schädlichen Code und schädliche Kommunikation zu verbergen. Fehlerbehafteter Drittanbietercode kann zu zusätzlichen Schwachstellen führen. Systemdienst-Deskriptortabelle (SSDT) Tabelle mit einem Array aus ­Zeigern auf die einzelnen System­aufrufhandler Abfangen der gesamten Benutzermoduskommunikation mit dem Kernel; die gleichen Probleme wie oben Kritische CPU-Register wie Steuerregister, Vektorbasisadressen-Register und modellspezifische Register Für Systemaufrufe, Virtualisierung, Aktivieren der CPU-Sicherheitsmerkmale wie SMEP usw. Wie vor; des Weiteren können damit wichtige CPU-Sicherheitsmerkmale und der Hypervisorschutz ausgeschaltet werden. Verschiedene Funktionszeiger im Kernel Wird für indirekte Aufrufe verschiedener interner Einrichtungen genutzt. Kann zum Einklinken in interne Kerneloperationen genutzt werden, was zu Instabilitäten führen kann und die Möglichkeit zur Einrichtung von Hintertüren gibt. → Kernelpatchschutz Kapitel 7 891
Komponente Legitime Nutzung Unerwünschte Nutzung Verschiedene globale Variablen im Kernel Konfiguration verschiedener Teile des Kernels, darunter auch einzelner Sicherheitsmerkmale Schadcode kann die Sicherheitsmerkmale ausschalten, z. B. durch einen Benutzermodusexploit, der ein willkürliches Überschreiben des Speichers ermöglicht. Prozess- und Modulliste Wird verwendet, um dem Benutzer in Programmen wie dem Task-Manager, Process Explorer und dem Windows-Debugger zu zeigen, welche Prozesse aktiv und welche Treiber geladen sind. Schadcode kann das Vorhandensein einzelner Prozesse oder Treiber vor dem Benutzer und den meisten Anwendungen verbergen, darunter auch vor Sicherheitssoftware. Kernelstacks Speichern von Funktionsargumenten, des Aufrufstacks (wohin eine Funktion zurückspringen soll) und von Variablen Die Verwendung eines nicht standardmäßigen Kernelstacks ist oft ein Zeichen eines ROP-Exploits (Return-Oriented Programming), bei dem ein Pivotstack verwendet wird. Fenster-Manager, Grafik­ system­aufrufe, -callbacks usw. Stellt die GUI-, GDI- und DirectX-­Dienste bereit. Bietet die schon zuvor beschriebenen Abfangmöglichkeiten, konzentriert sich aber insbesondere auf den Grafik- und den Fensterverwaltungsstack. Die gleichen Probleme wie bei anderen Arten von Hooks. Objekttypen Definitionen für die verschiedenen Objekte (wie Prozesse und Dateien), die das System mithilfe des Objekt-Managers unterstützt. Kann als weitere Technik für Hooks verwendet werden, die keine indirekten Funktionszeiger in den Datenabschnitten von Binärdateien angreifen und Code auch nicht direkt manipulieren. Hierbei bestehen die gleichen Probleme wie bei den zuvor erwähnten Hooks. Lokaler APIC Zum Empfang von Hardware­ interrupts auf dem Prozess, von Timerinterrupts und von Interprozessorinterrupts (IPI) Kann verwendet werden, um sich in die Timerausführung, in IPIs und andere Interrupts einzuklinken. Persistenter Code kann damit verdeckt auf dem Computer aktiv bleiben, indem er periodisch ausgeführt wird. Filter und Drittanbieter-Benachrichtigungscallbacks Wird von legitimer Drittanbieter-Sicherheitssoftware (und Windows Defender) genutzt, um Benachrichtigungen über Systemaktivitäten zu empfangen, und in manchen Faällen sogar dazu, bestimmte Aktionen zu blockieren bzw. abzuwehren. Dies ist eine zulässige Möglichkeit, um viel von dem zu erreichen, was der Kernelpatchschutz sonst verhindert. Kann von Schadcode genutzt werden, um sich in alle filterbaren Operationen einzuklinken und um durch periodische Ausführung auf dem Computer aktiv zu bleiben. 892 Kapitel 7 Sicherheit →
Komponente Legitime Nutzung Unerwünschte Nutzung Sonderkonfigurationen und -flags Verschiedene Datenstrukturen, Flags und Elemente legitimer Komponenten, die diesen Komponenten Sicherheits- und Abwehrgarantien geben. Kann von Schadcode genutzt werden, um Abwehrmaßnahmen zu umgehen und um Garantien und Erwartungen von Benutzermodusprozessen zu verletzen, z. B. um den Schutz eines Prozesses aufzuheben. KPP-Engine Code im Zusammenhang mit der Fehlerprüfung des Systems während einer Verletzung des Kernelpatchschutzes, mit der Ausführung der Callbacks für den Kernelpatchschutz usw. Durch Manipulation einzelner Teile des Systems, die vom Kernelpatchschutz verwendet werden, können unerwünschte Komponenten den Kernelpatchschutz zum Schweigen bringen, umgehen oder auf andere Weise lahmlegen. Tabelle 7–23 Allgemeine Beschreibung der vom Kernelpatchschutz abgesicherten Elemente Wenn der Kernelpatchschutz unerwünschten Code entdeckt, bringt er das System mit einem leicht erkennbaren Code zum Absturz. Dieser Code entspricht dem Bugcheck-Code 0x109, der für CRITICAL_STRUCTURE_CORRUPTION steht. Das Absturzabbild kann mit dem Windows-Debugger analysiert werden (siehe Kapitel 15 in Band 2). Es enthält einige Informationen über den beschädigten oder manipulierten Teil des Kernels. Alle weiteren Daten aber sind für Benutzer nicht verfügbar, sondern werden von den OCA- und WER-Teams von Microsoft (Online Crash Analysis und Windows Error Reporting) untersucht. Anstelle der vom Kernelpatchschutz abgelehnten Techniken können Drittanbieterentwickler die folgenden zulässigen Vorgehensweisen einsetzen: QQ Dateisystem(mini)filter Damit können Sie sich in sämtliche Dateioperationen einklinken, auch in das Laden von Abbilddateien und DLLs, die abgefangen werden können, um Schadcode im laufenden Betrieb zu entfernen oder das Lesen bekanntermaßen schädlicher ausführbarer Dateien und DLLs zu verhindern (siehe Kapitel 13 in Band 2). QQ Filterbenachrichtigungen für die Registrierung Damit können Sie sich in alle Registrierungsoperationen einklinken (siehe Kapitel 9 in Band 2). Sicherheitssoftware kann die Änderung kritischer Teile der Registrierung blockieren und Schadsoftware anhand von Registrierungszugriffsmustern und bekanntermaßen gefährlichen Registrierungsschlüsseln auf heuristische Weise erkennen. QQ Prozessbenachrichtigungen Sicherheitssoftware kann die Ausführung und Beendigung aller Prozesse und Threads auf dem System sowie das Laden und Entladen von DLLs beobachten. Mit den erweiterten Benachrichtigungen, die für Hersteller von Antivirus- und anderer Sicherheitssoftware hinzugefügt wurden, kann sie auch das Starten von Prozessen blockieren (siehe Kapitel 3). QQ Objekt-Manager-Filterung Sicherheitssoftware kann bestimmte Zugriffsrechte für Prozesse und Threads entfernen, um sich selbst gegen bestimmte Arten von Operationen zu schützen (siehe Kapitel 8 in Band 2). Kernelpatchschutz Kapitel 7 893
QQ LWF und WFP-Filter (NDIS Lightweight Filters/Windows Filtering Platform) Sicherheitssoftware kann alle Socketoperationen (Akzeptieren, Lauschen, Verbinden, Schließen usw.) und sogar die Pakete selbst abfangen. Mit LWF haben die Hersteller von Sicherheitssoftware Zugriff auf die rohen Ethernet-Framedaten, die von der Netzwerkkarte zum Kabel fließen. QQ Ereignisablaufverfolgung für Windows (Event Tracing for Windows, ETW) Mithilfe von ETW können Benutzermoduskomponenten viele Arten von Operationen mit sicherheitsrelevanten Eigenschaften verarbeiten und dann in beinahe Echtzeit darauf reagieren. Bei Abschluss eines Geheimhaltungsvertrags mit Microsoft und Teilnahme an verschiedenen Sicherheitsprogrammen sind für Prozesse, die von Antimalware geschützt werden, auch besondere, sichere ETW-Benachrichtigungen verfügbar. Dadurch erhalten die Hersteller Zugriff auf umfassendere Ablaufverfolgungsdaten. (Mehr über ETW erfahren Sie in Kapitel 8 von Band 2.) HyperGuard Auf Systemen mit virtualisierungsgestützter Sicherheit (siehe den Abschnitt »Virtualisierungsgestützte Sicherheit« weiter vorn in diesem Kapitel) wird Schadcode mit Kernelmodusrechten auf VTL 0 ausgeführt, also nicht mehr unbedingt auf derselben Sicherheitsebene wie ein Erkennungs- und Schutzmechanismus, der auch auf VTL 1 implementiert werden kann. Im Anniversary Update von Windows 10 (Version 1607) ist ein solcher Mechanismus unter der Bezeichnung HyperGuard vorhanden. Er unterscheidet sich durch einige bemerkenswerte Eigenschaften von PatchGuard: QQ Die Namen der Symboldateien und Funktionen, die HyperGuard implementieren, sind für jeden einsehbar, und der Code ist nicht verschleiert. Eine vollständige statische Analyse ist möglich. HyperGuard ist eine echte Sicherheitsgrenze und muss daher nicht auf Verschleierungstechniken zurückgreifen. QQ HyperGuard muss nicht indeterministisch ausgeführt werden, da dies aufgrund der zuvor genannten Eigenschaft keinen Vorteil mehr bieten würde. Die deterministische Vorgehensweise ermöglicht es HyperGuard sogar, das System genau zu dem Zeitpunkt zum Absturz zu bringen, an dem das unerwünschte Verhalten auftritt. Dadurch enthalten die Absturzdaten eindeutige und zielführende Informationen für Administratoren und die Analyseteams von Microsoft, z. B. den Kernelstack, der den Code zeigt, von dem das unerwünschte Verhalten ausgelöst wurde. QQ Aufgrund der vorstehenden Eigenschaft kann HyperGuard eine breite Palette von Angriffen erkennen, da Schadcode keine Möglichkeit mehr hat, Werte in einem exakt bemessenen Zeitfenster auf ihre korrekte Einstellung zurückzusetzen. Letztere Möglichkeit war eine ungünstige Nebenwirkung des Indeterminismus von PatchGuard. HyperGuard dient auch dazu, die Fähigkeiten von PatchGuard auf bestimmte Weise zu erweitern und dessen Fähigkeit zu verbessern, sich vor Angreifern zu verbergen, die es ausschalten wollen. Wenn HyperGuard Inkonsistenzen wahrnimmt, bringt es das System ebenfalls zum Ab- 894 Kapitel 7 Sicherheit
sturz, allerdings mit dem eigenen Code 0x18C (HYPERGUARD_VIOLATION). Auch hier ist es wiederum nützlich, zumindest grob zu wissen, was HyperGuard schützen kann. Eine entsprechende Übersicht finden Sie in Tabelle 7–24. Komponente Legitime Nutzung Unerwünschte Nutzung Ausführbarer Code im Kernel, seine Abhängigkeiten und Kerntreiber sowie die Importadresstabelle (IAT) dieser Komponenten Siehe Tabelle 7–23 Siehe Tabelle 7–23 Globale Deskriptortabelle (GDT) Siehe Tabelle 7–23 Siehe Tabelle 7–23 Interruptdeskriptortabelle (IDT) oder Interruptvektortabelle Siehe Tabelle 7–23 Siehe Tabelle 7–23 Kritische CPU-Register wie Steuerregister, GDTR, IDTR, Vektorbasisadressen-Register und modellspezifische Register Siehe Tabelle 7–23 Siehe Tabelle 7–23 Ausführbarer Code, Callbacks und Datenregionen im sicheren Kernel und seinen Abhängigkeiten, einschließlich HyperGuard selbst Windows-Standardkomponenten, die für die Verwendung von VTL 1 und des sicheren Kernels von entscheidender Bedeutung sind Ist Patchcode in diesen Kompo­ nenten vorhanden, so muss der Angreifer irgendeine Form von Zugriff auf eine ­Schwachstelle in VTL 1 haben, ­entweder durch die Hardware oder den Hyper­ visor. Damit können Device ­Guard, HyperGuard und ­Credential Guard ausgehebelt werden. Von Trustlets verwendete Strukturen und Features Gemeinsame Nutzung von Trustlets untereinander, zwischen Trustlets und dem Kernel oder zwischen Trustlets und VTL 0 Lässt darauf schließen, dass es in einem oder mehreren Trustlets Schwachstellen gibt, mit denen sich Features wie Credential ­Guard oder Shielded Fabric/ vTPM behindern lassen. Hypervisor-Strukturen und -Regionen Wird vom Hypervisor zur Kommunikation mit VTL 1 genutzt. Lässt darauf schließen, dass es eine Schwachstelle in einer VTL-1-Komponente oder dem Hypervisor selbst gibt, die von Ring 0 in VTL 0 aus zugänglich ist. Kernel-CFG-Bitmap Wird zur Identifizierung gültiger Kernelfunktionen verwendet, die das Ziel indirekter Funktionsaufrufe oder Sprünge sind (siehe die Beschreibung weiter vorn). Lässt darauf schließen, dass ein Angreifer in der Lage war, die VTL-1-geschützte KCFG-Bitmap durch einen Hardware- oder Hypervisor-Exploit zu manipulieren. Seitenverifizierung Dient zur Implementierung der HVCI-Aufgaben für Device Guard. Lässt darauf schließen, dass ein SKCI auf irgendeine Weise angegriffen wurde. Das kann zu einer Unterwanderung von Device Guard und zu nicht autorisierten IUM-Trustlets führen. → Kernelpatchschutz Kapitel 7 895
Komponente Legitime Nutzung Unerwünschte Nutzung NULL-Seite Keine Lässt darauf schließen, dass ein Angreifer den Kernel oder den sicheren Kernel auf irgendeine Weise dazu gebracht hat, die virtuelle Seite 0 zuzuweisen. Dies kann dazu verwendet werden, um NULL-Seiten-Schwachstellen in VTL 0 und VTL 1 auszunutzen. Tabelle 7–24 Allgemeine Beschreibung der von HyperGuard geschützten Elemente Auf Systemen mit aktivierter virtualisierungsgestützter Sicherheit gibt es noch eine andere wichtige Sicherheitseinrichtung, die im Hypervisor selbst implementiert ist, nämlich die Verhinderung der Ausführung nicht privilegierter Anweisungen (Non-Privileged Instruction Execution Prevention, NPIEP). Sie kümmert sich um besondere x64-Anweisungen, die missbraucht werden können, um die Kernelmodusadressen der GDT, IDT und LDT durchsickern zu lassen (SGDT, SIDT und SLDT). Bei Verwendung von NPIEP dürfen diese Anweisungen zwar immer noch ausgeführt werden (aus Kompatibilitätsgründen), geben aber nur eine auf dem Prozessor eindeutige Zahl zurück und nicht die wahre Kerneladresse der betreffenden Struktur. Dies dient als Schutz gegen KASLR-Informationslecks (Kernel-ASLR) durch lokale Angriffe. Wenn PatchGuard und HyperGuard einmal aktiviert sind, gibt es keine Möglichkeit, sie wieder auszuschalten. Da die Hersteller von Gerätetreibern im Rahmen des Debuggings jedoch Änderungen an einem laufenden System vornehmen können müssen, ist PatchGuard bei einem Systemstart im Debugmodus mit aktiver Verbindung für das Remotekerneldebugging nicht aktiviert. Auch HyperGuard wird deaktiviert, wenn der Hypervisor im Debugmodus mit angeschlossenem Remotedebugger startet. Zusammenfassung Windows bietet eine breite Palette von Sicherheitsfunktionen, die die Kernanforderungen sowohl von staatlichen Stellen als auch von kommerziellen Unternehmen erfüllen. In diesem Kapitel haben Sie sich einen Überblick über interne Komponenten verschafft, auf denen diese Sicherheitsfunktionen beruhen. In Kapitel 8 von Band 2 schauen wir uns die einzelnen Mechanismen an, die überall in Windows verstreut sind. 896 Kapitel 7 Sicherheit
Index ! 45 64-Bit-Adressraumlayouts 406 .NET Framework 6 .process 122 A AAM (Admin Approval Mode) 848 Abbilder Abbildverschiebung 418 HAL 89 laden siehe Abbildlader native Abbilder 81 öffnen 151 Randomisierung des Benutzeradressraums 418 Abbildlader Ablaufsteuerungsschutz 872 Aktivierungskontext 182 API-Sets 194 Binary Planting 179 Datenbank der geladenen Module 183 DLL-Namensauflösung 179 DLL-Namensumleitung 181 DLL-Suchreihenfolge 182 frühe Initialisierung 176 Importanalyse 188 Prozessinitialisierung nach dem Import 190 sicherer DLL-Suchmodus 179 Switchback 191 Überblick 173 untersuchen 175 Abbildverschiebung 418 Ablaufsteuerungsschutz 862 Abbildlader 872 CFG-Bitmap 866 CFG-Unterdrückung 863 Exportunterdrückung 871 Kernel-CFG 874 Überblick 862 untersuchen 863 verstärken 870 Ablaufverfolgung Prozessstart 167 SuperFetch 543 Abschnittsobjekte 462 Abwehrmaßnahmen 421 Abwehrmaßnahmen gegen Exploits Abwehrmaßnahmen auf Prozessebene 856 CFG siehe Ablaufsteuerungsschutz CFI (Control Flow Integrity) 861 Überblick 856 Zusicherungen siehe Zusicherungen Address Windowing Extensions (AWE) 25, 368 Administratorgenehmigungsmodus 848 Administratorrechte 847 Adressraum 64-Bit-Adressraumlayouts 406 Abbildverschiebung 418 Adressnutzung beobachten 410 ARM-Adressraumlayout 405 Arten von Daten 396 Benutzeradressräume siehe Benutzeradressräume dynamische Zuweisung 408 Einschränkungen für virtuelle x64-Adressen 408 Grenzwerte für Adressen festlegen 413 ImageBias 418 kanonische Adressen 408 Kontingente 415 Prozesse erstellen 156 PTEs 404 Seitentabelleneinträge 404 Sitzungen 401 x86-Adressraumlayout 397, 401 x86-Sitzungsraum 401 Adressübersetzung ARM 434 PTEs 426 Schreibbit 428 Seitentabellen 426 TLB 429 Überblick 422 untersuchen 430 x64 433 x86 422 Affinitäts-Manager 382 Affinitätsmasken erweiterte Affinitätsmasken 311 symmetrisches Multiprocessing 59 Threads 309 Aktivierungskontext 182 897
Anmeldeinformationsanbieter 109 Anmeldung aktive Anmeldesitzungen untersuchen 830 Benutzerauthentifizierung 828 Gruppenansprüche 834 Kerberos 829 MSV1_0 828 SAS 826 sichere Authentifizierung 833 Überblick 824 WBF 835 Windows-Anmeldeprozess 109 Windows Hello 837 Winlogon-Initialisierung 826 Anspruchsgestützte Zugriffssteuerung 772 Anwendungen siehe auch Prozesse Anwendungscontainer siehe Anwendungscontainer AppID 880 AppLocker 881 Desktopanwendungen 115 klassische Anwendungen 115 moderne Apps 115 umfangreiche Adressräume 399 UWP-Apps 794 Anwendungscontainer Broker 822 Fähigkeiten 810 Handles 818 Lowbox 149 Objektnamensräume 816 Token 801 Überblick 793 UWP-Apps 794 UWP-Prozesse 796 APIs API-Sets 194 AuthZ 771 COM 5 .NET Framework 6 Überblick 4 Windows-Laufzeit 5 API-Sets 194 AppID 880 AppLocker 881 Arbeitsfactorys 335 Arbeitssätze Arbeitsspeicher 472 Auslagerung bei Bedarf 472 Balance-Set-Manager 482 Benachrichtigungsereignisse 485 Größe anzeigen 479 Platzierungsrichtlinien 477 Prefetcher 472 ReadyBoot 473 Swapper 482 Systemarbeitssätze 483 898 Index Überblick 472 verwalten 477 Arbeitsspeicher Abschnittsobjekte 462 AWE (Address Windowing Extensions) 25, 367 Enklaven 536 Grenzwerte 512 IRQL-Überprüfung 641 Komprimierung 102, 515 Partitionen 523 Poolnachverfolgung 640 Simulation geringer Ressourcen 641 sonstige Prüfungen 642 spezieller Pool 639 Stacks 454 virtueller Arbeitspeicher 23 Windows-Clienteditionen 513 WSRM 248 zusammenführen 526 Architektur Benutzermodus 53 Kernelmodus 53 Komponenten 69 Überblick 53, 69 VBS 66 ARM Adressraumlayout 405 Adressübersetzung 433 ASLR 416, 856, 867 Asymmetrisches Multiprocessing 58 Asynchrone E/A 586 Atomtabelle 808 Attribute Anwendungscontainer 805 konvertieren 145 Trustlets 139 Auflistung 645 Ausführungsbereite Threads 256 Ausführungsschutz 362 Ausgabe dump 44 ETHREAD 218 Gerätebäume 647 KTHREAD 218 Silokontext 208 Ausgelagerter Pool 370 Ausgleichssatz-Manager, Prioritätserhöhung 277 Auslagerung bei Bedarf 472 Auslagerungsdatei Größe 452 Reservierung 509 Überblick 442 untersuchen 445 UWP-Auslagerungsdatei 447 virtuelle Auslagerungsdatei 448 Auslassen der durchsuchenden Überprüfung 782 Ausrufezeichen 45
Auswählen Prozessoren 320 Threads 299, 319 Authentifizierung aktive Anmeldesitzungen untersuchen 830 Benutzer 827 Kerberos 829 MSV1_0 828 Richtlinien 710 AuthZ 771 Autoboost 286 Automatische Rechteerweiterung 852 AWE (Address Windowing Extensions) 25, 367 B Balance-Set-Manager, Arbeitssätze 482 Bandbreite reservieren 634 Bäume 645 Bedingte Zugriffssteuerungseinträge 772 Beenden E/A 619 Prozesse 172 TerminateJobObject 199 Threads 291, 619 Befehle ! 45 ~ 225, 233 !address 868 at 8 AuditPol 790 Bang-Befehle 45 !ca 467, 470 !cpuinfo 260 cron 8 !dd 433 dd 432 !devnode 647 !devobj 579, 602 !devstack 598 docker 211 !dq 431 !drvobj 579, 587, 594 dt 45, 87, 120, 203 dump 44 !file 467 !fileobj 585 g 79, 297 !handle 121, 467, 821 !heap 385 !heap -i 389 !heap -s 387 !irp 596, 600, 621 !irpfind 599 !job 201, 298, 333 k 79, 234 !list 186 lm 125 !lookaside 377 !memusage 469 !numa 303 !object 577 !partition 525 !pcr 86, 292 !peb 122, 185 !pfn 508 !pocaps 686 poolmon 372 !poolused 374 !popolicy 688 powercfg /a 681 powercfg /h 680 powercfg /list 687 !prcb 86 !process 121, 201, 219, 293, 595 .process 122 !pte 426, 431, 535 q 48 !ready 256 runas 113 !sd 757 !session 402 !silo 210 !smt 302 start 243 !sysptes 404 !teb 224 !thread 121, 219, 224, 293, 595 !token 734 u 79 !vad 459 ver 2 !verifier 640 !vm 348, 403 !wdfkd.wdfldr 667 winver 2 !wsle 481 Benachrichtigungsereignisse 485 Benutzer aktive Anmeldesitzungen untersuchen 830 Authentifizierung 827 E/A abbrechen 618 Kerberos 829 mehrere Sitzungen 32 MSV1_0 828 schneller Benutzerwechsel 33, 546 SuperFetch 545 Benutzeradressraum Abbildrandomisierung 418 Abwehrmaßnahmen 421 EMET 421 Heaprandomisierung 420 IRPs 607 Index 899
Kernel 420 Ladeadresse berechnen 419 Layout 416 Stackrandomisierung 420 Überblick 416 Unterstützung für Randomisierung 421 untersuchen 417 Benutzerkontensteuerung Administratorgenehmigungsmodus 848 Administratorrechte 847 automatische Rechteerweiterung 852 Dateivirtualisierung 839 Einstellungen 853 Genehmigungserweiterung 848 Rechteerweiterung 848 Registrierungsviertualisierung 839, 845 Überblick 839 Über-die-Schulter-Rechteerweiterung 848 Benutzermodus Architektur 53 Vergleich mit Kernelmodus 26, 52 Benutzermodusdebugging Debugging Tools for Windows 43 Threads anzeigen 233 Benutzerstacks 455 Besitzerrechte-SIDs 765 Besonderer Pool 370 Betriebssystem Modell 52 Multitasking 57 Skalierbarkeit 60 Zusicherungen 876 Binary Planting 179 BNO-Isolierung 822 Broker 822 Buckets 382 Bumps 631, 633 Bustreiber 566 C CBAC (Claims-Based Access Control) 772 CC (Common Criteria) 699 CFG siehe Ablaufsteuerungsschutz CFG-Bitmap 866 CFG-Unterdrückung 863 CFI (Control Flow Integrity) 861 Clienteditionen 513 COM 5 Common Criteria (CC) 699 Compiler 876 Component Object Model (COM) 5 Container Container 211 erstellen 210 Hilfsfunktionen 211 900 Index Isolierung 205 Kontext 207 Objekte 204 Überblick 204 Überwachung 209 Containerbenachrichtigung 634 Control Flow Integrity (CFI) 861 Copy-on-Write 366 CPU Aushungern 276 CPU-Rate 330 CPU-Sätze 313 Credential Guard Authentifizierungsrichtlinien 710 Kennwörter 706 Kerberos-Armoring 710 NTOWF/TGT 707 sichere Kommunikation 708 Überblick 705 UEFI 710 CSR_PROCESS 117, 123 CSR_THREAD 217, 229 D Dateien Auslagerungsdatei siehe Auslagerungsdatei Dateizuordnungsobjekte 22 E/A in zugeordneten Dateien 589 im Speicher zugeordnete Dateien 358 INF-Dateien 658 Katalogdateien 660 Virtualisierung 838 zwischenspeichern 589 Dateiobjekte 582 Dateizuordnungsobjekte 22 Daten Adressraum 396 laden 541 Datenausführungsverhinderung 362 Datenbanken Abbildlader 183 Datenbank der geladenen Module 183 Dispatcherdatenbank 255 Datenstrukturen CSR_PROCESS 117, 123 CSR_THREAD 217, 229 DXGPROCESS 117 EPROCESS 117 ETHREAD 117, 216 KPROCESS 118 KTHREAD 216 PFN 504 !process 121 W32PROCESS 117, 125 Deadline Scheduling 285
Debugging Benutzermodus 43, 233 DebugActiveProcess 43 DebugBreak 215 Debugging Tools for Windows 43 Heap 389 Kernelmodus 43, 234 nicht beendbare Prozesse 619 Typinformationen untersuchen 45 Deferred Procedure Calls (DPCs) E/A 563 Stacks 457 DEP (Data Execution Protection) 362 Dependency Walker exportierte Funktionen 38 HAL-Abbildabhängigkeiten 90 Teilsysteme 70 Designziele 51 Desktopanwendungen 115 Device Guard 712 DFSS (Dynamic Fair Share Scheduling) 326 Dienste Dienstprozesse 109 Dienststeuerungs-Manager 107 Speicher-Manager 350 Systemdienstdispatcher 79 Trustlets 141 Überblick 8 untersuchen 109 Dienstprozesse 109 Dienststeuerungs-Manager 107 Direct Switch 287 Direkter Kontextwechsel 22 Dispatcher 240 Dispatcherdatenbank 255 Dispatchereignisse 267 Dispatchroutinen Gerätetreiber 578 IRPs 594 DLLs analysieren 188 Anmeldeinformationsanbieter 109 DllMain 173 Namensauflösung 179 Namensumleitung 181 Ntdll.dll 78 sicherer DLL-Suchmodus 179 Suchreihenfolge 182 Teilsystem-DLLs 54 Teilsysteme 70 Überblick 8 DPCs (Deferred Procedure Calls) E/A 563 Stacks 457 DSRM siehe Verzeichnisdienstwiederherstellung Durchsatz 632 DXGPROCESS 117 Dynamic Fair Share Scheduling (DFSS) 326 Dynamische gleichmäßige Planung 326 Dynamische Prozessoren 333 Dynamische Zugriffssteuerung 770 Dynamische Zuweisung 408 E E/A abbrechen 617 asynchron 586 Containerbenachrichtigungen 634 Dateien zwischenspeichern 589 DPCs 563 E/A in zugeordneten Dateien 589 E/A-Manager 556 Einlagerungs-E/A 440 Energietreiber 686 Energieverfügbarkeitsanforderungen 692 Energieverwaltung 679, 689 Energieverwaltungsframework 690 Energiezuordnungen 684 Energiezustände 679 Geräteauflistung 645 Gerätebäume 645 Geräteknoten 648 Gerätestacks 649 INF-Dateien 658 IRPs siehe IRPs IRQLs 561, 641 Katalogdateien 660 Komponenten 555 Leistungsstatus 691 Parallelität 623 Plug & Play 643 PnP-Unterstützung 644 Prioritätserhöhung 269 Scatter/Gather-E/A 589 schnelle E/A 587 synchron 586 Systemfähigkeiten 686 Threadagnostisch 616 Thread beenden 619 Treiberisntallation 656 Treiberüberprüfung 637, 640 Treiberunterstützung 644, 655 Überblick 555 verarbeiten 558 Vervollständigungsports 622 E/A-Anforderungspakete siehe IRPs E/A in zugeordneten Dateien 589 E/A-Manager 556 Echtzeitprioritäten 243 Editionen 61 Einfrieren 296 Eingabeaufforderung 15 Index 901
Eingeschränkte Token 743 Einlagerungs-E/A 440 Einstellungen Adressgrenzwerte 413 Benutzerkontensteuerung 853 EPROCESS 154 PEB 159 EMET (Enhanced Mitigation Experience Toolkit) 421 Energiezuordnungen 684 Enhanced Mitigation Experience Toolkit (EMET) 421 Enklaven 536 EPROCESS 117, 154 Ereignisse Benachrichtigungsereignisse 485 Dispatchereignisse 267 Erweiterte 64-Bit-Systeme 56 Erweiterte Affinitätsmasken 311 Erweiterte Überwachungsrichtlinie 792 ETHREAD 117, 216 Exekutive 81 Exekutivprozessobjekt 154 Exekutivressourcen 271 Experimente Abbildlader 175 Ablaufsteuerungsschutz 863 Abschnittsobjekte 463 Adressnutzung 410 Adressübersetzung 430 aktive Anmeldesitzungen 830 Anwendungscontainertoken 801 Arbeitssätze 478 Arbeitsspeicher 345 Atomtabelle 808 ausführungsbereite Threads 256 aushungern der CPU 277 Auslagerungsdatei 443, 452 auslassen der durchsuchenden Überprüfung 782 Benachrichtigungsereignisse 486 Benutzeradressraum 416 Benutzermodusdebugger 233 Broker 822 CFG-Bitmap 868 CPU-Sätze 313 CSR_PROCESS 124 CSR_THREAD 229 Dateiobjekte 583 Dateivirtualisierung 843 Datenausführungsverhinderung 365 Datenbank der geladenen Module 185 Dienste 108 Dienstprozesse 109 Dispatchroutinen 594 DLL-Suchreihenfolge 182 902 Index DSS 328 Durchsatz 632 Energiefähigkeiten 686 Energieverfügbarkeitsanforderungen 693 Energiezustände 681 Energiezustandszuordnung 685 EPROCESS 119 ETHREAD 218 exportierte Funktionen 38 Fähigkeiten von Anwendungscontainern 813 gefilterte Administratortoken 744 Gerätebäume 647 Geräteknoten 653 Geräteobjekte 576 Gerätetreiber 95 geschützte Prozesse 132, 237 globale Überwachungsrichtlinie 790 Grenzwerte der CPU-Rate 331 Grenzwerte für Adressen festlegen 413 HAL-Abbildabhängigkeiten 90 Heap 384 im Speicher zugeordnete Dateien 359 INF-Dateien 658 Integritätsebenen 724 in Token gespeicherte Handles 819 IRPs 595, 599 Jobs 200 Katalogdateien 660 Kernelmodusdebugger 235 Kernelstacks 456 Kerneltypinformationen 45 KMDF-Treiber 666 KPCR 86 KPRCB 86 KTHREAD 218 Ladeadresse berechnen 419 Leerlaufthreads 292 Lese- und Schreibvorgänge in der PrefetchDatei 475 Liste der freien Seiten 492 Liste geänderter Seiten 494 Look-Aside-Listen 377 Maximale Anzahl an Threads erstellen 455 MMCSS-Prioritätsschub 284 nicht beendbare Prozesse debuggen 619 Nullseitenlisten 493 NUMA-Prozessoren 303 Objektzugriffsüberwachung 787 PEB 122 PFN 490 PFN-Einträge 508 Poolgröße 371 Poollecks 375 Prioritätserhöhungen von GUI-Threads 275 Prioritätserhöhung für Vordergrundthreads 271
Prioritätsschübe und -bumps 633 Privilegien aktivieren 781 Process Explorer 17 Programme auf niedriger Integritätsebene starten 740 Prozessaffinität 309 Prozessbaum 13 Prozessdatenstrukturen 119 Prozessstart verfolgen 167 PTEs 404 Quantum konfigurieren 265 schnelle E/A 587 Seitenheap 391 Seitenprioritäten 501 Sicherheitsattribute von Anwendungscontainern 806 Sicherheitsdeskriptoren 755 SIDs 722 Silokontext 208 Sitzungen 401 SMT-Prozessoren 302 Speicherkomprimierung 522 Speicherpartitionen 525 Speicher zuweisen 354 SRPs 889 Stacks 600 Standby-Seitenlisten 494 Steuerbereiche 467 Systemdienstdispatcher 79 Taktintervall 258 Taktzyklen pro Quantum 260 Task-Manager 9 TEB 223 Teilsysteme 70 Testbuild 64 Threadpools 338 Threadprioritäten 245 Threads einfrieren 297 Threadstatus 250 Token 732 Treiber 568 Treiberobjekte 579 Trustlets identifizieren 143 UMDF-Treiber 667 Unterstützung für umfangreiche Adressräume 399 UWP-Auslagerungsdatei 448 UWP-Prozesse 799 VADs 458 Vergleich der Leistungsüberwachung im Kernel- und Benutzermodus 29 Vertrauens-SIDs 760 Virtuelle Auslagerungsdatei 448 Virtuelle Dienstkonten 746 Windows-Editionen 63 Zugriffsmaske 719 Exportunterdrückung 871 F Fähigkeiten 63 Fehlerbehebung 375 Fehlercodes 877 Fehlertolerante Heaps 394 Fenster 15 Fibers 21 Filter 566 Filtertreiber 566 Firmware 32 Flags 145 Frameworks Energieverwaltungsframework 690 .NET Framework 6 WBF (Windows Biometric Framework) 835 Freie Seiten Listen 492 Speicher-Manager 352 Freiwilliger Wechsel 288 FRS siehe Dateireplikationsdienst FTH (fehlertolerante Heaps) 394 Funktionen AllocConsole 71 AvTaskIndexYield 285 BaseThreadInit 179, 190 ConvertThreadToFiber 21 CreateFiber 21 CreateFile 37 CreateProcess 113, 144, 149, 176 CreateProcessAsUser 113, 155 CreateProcessInternal 114 CreateProcessInternalW 144, 145, 149, 162, 167 CreateProcessWithLogonW 113 CreateProcessWithTokenW 113 CreateRemoteThread 215 CreateRemoteThreadEx 216, 230 CreateThread 215, 222, 232 Csr 80 DbgUi 80 DebugActiveProcess 43 DebugBreak 215 DeviceIoControl 82 DllMain 173 Etw 81 Ex 82 ExitProcess 172 Exportiert 38 GetQueueCompletionStatus 197 GetSystemTimeAdjustment 259 GetThreadContext 20 GetVersionEx 2 HeapAlloc 379 HeapCreate 379 HeapDestroy 379 HeapFree 379 Index 903
HeapLock 379 HeapReAlloc 379 HeapUnlock 379 HeapWalk 379 Inbv 82 Io 82 IoCompleteRequest 270 Iop 82 Ke 84 KeStartDynamicProcessor 334 KiConvertDynamicHeteroPolicy 324 KiDeferredReadyThread 309, 320 KiDirectSwitchThread 287 KiExitDispatcher 269, 287 KiProcessDeferredReadyList 309 KiRemoveBoostThread 269, 281 KiSearchForNewThreadOnProcessor 320 KiSelecthreadyThreadEx 300 KiSelectNextThread 299 Ldr 79 LdrApplyFileNameRedirection 195 Mi 82 MiZeroInParallel 343 NtCreateProcessEx 116, 134 NtCreateThreadEx 230 NtCreateUserProcess 116 NtCreateWorkerFactory 337 OpenProcess 43 PopInitializeHeteroProcessors 323 Präfixe 98 PsCreateSystemThread 216 PspAllocateProcess 117, 154 PspComputeQuantum 262 PspCreatePicoProcess 116 PspInitializeApiSetMap 195 PspInsertProcess 117, 159 PsTerminateSystemThread 216 QueryInformationJobObject 199 ReadFile 28 ReadProcessMemory 22 ResumeThread 296 Rtl 80 RtlAssert 65 RtlCreateUserProcess 116 RtlGetVersion 62 RtlUserThreadStart 232 RtlVerifyVersionInfo 62 SetInformationJobObject 309 SetPriorityClass 243 SetProcessAffinityMask 309 SetProcessWorkingSetSize 248 SetThreadAffinityMask 309 SetThreadIdealProcessor 313 SetThreadSelectedCpuSets 314 Sichere Systemaufrufe 80 SuspendThread 296 SwitchToFiber 21 904 Index Systemdienste 81 SystemParametersInfo 199 Teilsystem-DLLs 71 TerminateJobObject 199 TerminateProcess 173 TermsrvGetWindowsDirectoryW 190 TimeBeginPeriod 259 TimeSetEvent 259 TpAllocJobNotification 197 Treiber 565 Überblick 8 UserHandleGrantAccess 199 VerifyVersionInfo 2, 62 VirtualLock 356 WaitForMultipleObjects 288 WaitForSingleObject 288 Wow64GetThreadContext 21 WriteProcessMemory 22 ZwUserGetMessage 234 G Gefilterte Administratortoken 744 Gemein genutzte PTEs 530 Gemeinsam genutzter Speicher 358 Gemeinsam nutzbare Seiten 352 Geräte Auflistung 645 Bäume 645 Devnodes 645 Geräteknoten 648 Gerätestacks 649 öffnen 582 Unterstützung 644 Geräteobjekte 574 Gerätetreiber Benutzeradressraum 607 Bustreiber 566 Dateiobjekte 582 Dispatchroutinen 578, 594 Energieverwaltung 685 Filtertreiber 566 Funktionstreiber 566 Geräteobjekte 574 Geräte öffnen 582 geschichtete Treiber 567, 613 installieren 555, 663 IRPs 603, 613 Klassentreiber 567 KMDF 664 laden 661 Porttreiber 567 Routinen 572 Treiberobjekte 574 Treiberüberprüfung siehe Treiberüberprüfung Typen 565
Überblick 92, 565 UMDF 565, 664 universelle Windows-Treiber 95 Unterstützung 644, 655 untersuchen 95, 570 WDF siehe WDF WDK (Windows Driver Kit) 49 WDM 93, 566 Geringe Ressourcen simulieren 641 Gesamter zugesicherter Speicher 356 Geschichtete Treiber IRPs 613 Überblick 567 Geschützte Prozesse PPLs 128 Process Explorer 132 Threads 237 Überblick 126 Globale Überwachungsrichtlinie 790 gMSAs siehe Gruppenverwaltete Dienstkonten Granularität 357 Größe gesamter zugesicherter Speicher 452 Pools 370 Speicher-Manager 343 umfangreiche Adressräume 399 Große Seiten 344 Gruppen Ansprüche 834 Claims 834 DFSS 326 dynamische Prozessoren 333 Grenzwerte der CPU-Rate 330 Prozessoren 305 Zeitplanung 324 GUI Sicherheitseditoren 768 Threads 274 GUIDs 191 besonderer Pool 370 Buckets 382 Debugging 390 fehlertolerante Heaps 394 Größe 370 HeapAlloc 379 HeapCreate 379 HeapDestroy 379 HeapFree 379 HeapLock 379 HeapReAlloc 379 HeapUnlock 379 HeapWalk 379 Lecks 375 Look-Aside-Listen 377 Low-Fragmentation-Heap 381 nicht ausgelagerter Pool 370 NT-Heaps 380 Nutzung 372 poolmon 372 Prozesse 379 Randomisierung 420 Segmentheaps 383 Seitenheap 391 Sicherheit 388 Spezieller Pool 639 Synchronisierung 381 Threadpools 335 Typen 380 Überblick 370, 380 untersuchen 384 Verfolgung 640 Heterogenes Multiprocessing 59 Heterogene Zeitplanung 322 Hierarchien 199 Host 75 Hybridjobs 205 HyperGuard 894 Hypervisor 30 H I HAL IBAC (Identity-Based Access Control) 771 Idealer Knoten 313 Idealer Prozesser 312 Identifizierung AppID 880 Trustlets 143 Identitäten 140 Identitätsgesstützte Zugriffssteuerung 771 Identitätswechsel 741 ImageBias 418 Immersive Prozesse 115 Im Speicher zugeordnete Dateien 358 INF-Dateien 658 Infrastruktur öffentlicher Schlüssel siehe PKI Abhängigkeiten von Abbildern 90 Überblick 88 Handles Anwendungscontainer 818 in Token gespeicherte Handles 819 Hardware Firmware 32 Kernelunterstützung 88 Hardwareabstraktionsschicht siehe HAL Hashes 808 Heaps Affinitäts-Manager 382 ausgelagerter Pool 370 Index 905
Initialisierung Abbildlader 176, 190 Prozesse erstellen 165 Speicherenklaven 538, 542 Teilsystem 162 Winlogon 826 Integrierte Trustlets 140 Integritätsebenen 724, 740 Interne Synchronisierung 350 Interrupt Request Levels (IRQLs) 561, 641 IoCompletion 623 IRPs 588 Benutzeradressraum 607 Dispatchroutinen 594 geschichtete Treiber 613 IRP-Fluss 597 Stackpositionen 592 Stacks 600 Synchronisierung 610 Überblick 590, 603 untersuchen 595, 599 IRQLs 561, 641 Isolierung 205 J Jobs erstellen 199 Grenzwerte 197 Hierarchien 199 Hybridjobs 205 Überblick 23, 196 untersuchen 201 K Kanonische Adressen 408 Katalogdateien 660 Kennwörter 706 Kerberos Armoring 710 Authentifizierung 829 Kerne 59 Kernel Benutzeradressraum 420 Benutzermodusdebugging 43 Debugging 42 Debugging Tools for Windows 43 Hardwareunterstützung 88 Kernel-CFG 874 Kernelmodusdebugging 44 KPCR 85 KPRCB 85 LiveKd 48 Objekte 85 906 Index Prozesse 118 Prozessstrukturen 157 sicherer Kernel 67 Stacks 456 Symbole 42 Testbuild 64 Threads 234 Typinformationen 45 Überblick 85 Kernelmodus Architektur 53 Vergleich mit Benutzermodus 26, 52 Kernelpatches HyperGuard 894 PatchGuard 890 Überblick 889 Kernelprozessorsteuerungsblock (KPRCB) 85 Kernelprozessorsteuerungsregion (KPCR) 85 Klassentreiber 567 Klassifizierung 529 Klassische Anwendungen 115 Kleine Seiten 343 KMDF 664 Knoten Geräteknoten 648 idealer Knoten 313 Prozessoren 58 Kommunikation 708 Komponenten Architektur 68 E/A 555 Sicherheit 700 Speicher-Manager 342 SuperFetch 543 Konfiguration DFSS 327 Quantum 264 Konsolen 75 Konten auslassen der durchsuchenden Überprüfung 782 Kontorechte 773 Privilegien 773 Superprivilegien 783 Kontext Abbildlader 182 Aktivierungskontext 182 Direct Switch 287 direkter Kontextwechsel 22 Jobs 207 Kontextwechsel 240, 286 Threads 20 Kontextwechsel 240, 286 Kontingente 415 Kopieren beim Schreiben 366 KPCR 85
KPRCB 85 KPROCESS 118 KTHREAD 216 L Ladeadresse 419 Laden Abbilder siehe Abbildlader Daten 541 Ladeadresse 419 Treiber 661 Layout 64-Bit-Adressraumlayouts 406 ARM-Adressraumlayout 405 Benutzeradressraum 416 x86-Adressraumlayout 397, 401 Lecks 375 Leerlaufprozess 100 Leerlaufthreads 292, 301 Legacy-Standby 683 Leistung stabil 549 Status 691 SuperFetch 548 Leistungsüberwachung Kernelmodus und Benutzermodus im Vergleich 29 Überblick 40 Letzter Prozesser 312 LFH (Low-Fragmentation-Heap) 381 Linux-Teilsysteme 76 Liste geänderter Seiten 494 Listen Look-Aside-Listen 377 Mindest-TCB-Liste 131 Seitenlisten 493 LiveKd 48 Logischer Prefetcher 473 Lokaler Threadspeicher (TLS) 20 Look-Aside-Listen 377 Look-Aside-Übersetzungspuffer 429 LowBox siehe Anwendungscontainer Low-Fragmentation-Heap 381 M Mehrere Sitzungen 32 Mehrprozessorsysteme Affinitätsmasken 59, 309 Anzahl der Prozessoren pro Gruppe 307 Asymmetrisch 57 CPU-Sätze 313 erweiterte Affinitätsmasken 311 Heterogen 59 idealer Knoten 313 idealer Prozesser 312 letzter Prozessor 312 NUMA-Systeme 302 Prozessoren auswählen 320 Prozessorgruppen 59, 305 Prozessorstatus 308 Skalierbarkeit des Schedulers 308 SMT 301 symmetrisch 57 Überblick 301 MFA siehe Mehrstufige Authentifizierung Mindest-TCB-Liste 131 Minimale Prozesse 116, 134 MMCSS (Multimedia Class Scheduler Service) 266, 282 Moderne Apps 115 Moderne Prozesse 115 Moderner Standby 683 Module 183 MSAs siehe Verwaltete Dienstkonten MSV1_0-Authentifizierung 828 Multimedia Class Scheduler Service (MMCSS) 266, 282 Multitasking 57 N Namensraum 816 Native Abbilder 81 Native Prozesse 116 Nicht ausgelagerter Pool 370 Nicht beendbare Prozesse 619 Ntdll.dll 78 NT-Heaps 380 NTOWF/TGT 707 Nullforderungsseiten 352 Nullseitenlisten 493 NUMA (Non-Uniform Memory Architecture) Prozessoren 303 Systeme 302 Überblick 461 NX 362 O Objekte Abschnittsobjekte 462 DAC 770 Dateiobjekte 582 Dateizuordnungsobjekte 22 Dispatchroutinen 578 EPROCESS 154 Exekutivprozessobjekt 154 Geräteobjekte 574 Index 907
IoCompletion 623 Kernelobjekte 85 Namensräume 816 Objektzugriffsüberwachung 787 Sicherheit 714 Sicherheitsdeskriptoren 751 Überblick 33 virtuelle Dienstkonten 745 Zugriffsprüfung 716 Öffnen Abbilder 151 Eingabeaufforderung 15 Geräte 582 OneCore 3 OTS-Rechteerweiterung 848 P PAE (Physical Address Extension) 422 Parallelitätswert 623 Parametervalidierung 145 Partitionen 523 PatchGuard 890 PCB 118 PCR 292 PDE 425 PDPE 425 PDPT 424 PEB 117 einrichten 159 Überblick 117 untersuchen 123 PFN Auslagerungsdatei 509 Datenstrukturen 504 Einträge 508 Schreibthread für geänderte Seiten 502 Schreibthread für zugeordnete Seiten 502 Seitenlisten 491 Seitenprioritäten 500 Seitenstatus 488 Überblick 487 untersuchen 490 Physische Adresserweiterung (PAE) 422 Pico Prozesse erstellen 116 Teilsysteme 76 Überblick 134 Planungskategorie 283 Platzierungsricthlinien 477 Plug & Play Auflistung 645 Bäume 645 Geräteknoten 648 Gerätestacks 649 908 Index INF-Dateien 658 Katalogdateien 660 Treiber 644 Treiberinstallation 555 Treiberunterstützung 655 Überblick 643 Unterstützung 644 Pools Größe 371 Nutzung 372 Threads 338 Portierbarkeit 56 Ports Porttreiber 567 Vervollständigungsports 622 Porttreiber 567 PPL (Protected Processes Light) 128 Präfixe 98 Prefetcher 472 Prioritäten Bandbreitenreservierung 634 Bumps 631, 633 Durchsatz 632 E/A 627 Echtzeit 243 Prioritätsschübe 631, 633 Prioritätsstufen 240 Prioritätsumkehr 630 Seiten 499 Strategien 628 SuperFetch 546 untersuchen 244 Prioritätserhöhung anwenden 279 Ausgleichssatz-Manager 276 aushungern der CPU 276 Autoboost 286 Deadline Scheduling 285 Dispatchereignisse 267 E/A 269 entfernen 280 Exekutivressourcen 271 GUI-Threads 274 MMCSS 266, 282 Multimedia 282 Planungskategorie 283 Prioritätsumkehr 276 sperren 269 Spiele 282 Überblick 266 Unwait-Erhöhungen 268 Vordergrundthreads 271 Prioritätsumkehr E/A-Priorität 630 Prioritätserhöhung 275 Private Seiten 352
Privilegien 773 auslassen der durchsuchenden Überprüfung 782 Superprivilegien 783 Process Explorer 16 Programme siehe Anwendungen Protected Process Light (PPL) 128 Protokollierung 544 Prototyp-PTEs 438 Prozessbaum 13 Prozesse Abbilder öffnen 151 Abbildlader 175, 182 Abwehrmaßnahmen auf Prozessebene 856 Adressraum 156 Attribute konvertieren 145 beenden 172 CSR_PROCESS 117, 123 DLL-Suchreihenfolge 182 DXGPROCESS 117 EPROCESS 117, 154 erstellen 113, 144 ETHREAD 117 Exekutivprozessobjekt 154 Flags konvertieren 145 geschützte Prozesse 132, 237 Heap 379 immersive Prozesse 115 initialisieren 165 Kernelprozesse 118 Kernelprozessstruktur 157 Konsolenhost 75 KPROCESS 118 Mindest-TCB-Liste 131 minimale Prozesse 116, 134 moderne Prozesse 115 native Prozesse 116 nicht beendbare Prozesse 619 Parametervalidierung 145 PCB 118 PEB 122, 159 !process 121 Process Explorer 16, 132 Prozessbaum 13 prozessübergreifender Speicherzugriff 352 Reflexion 552 Start verfolgen 167 Task-Manager 9 Teilsystem initialisieren 162 Überblick 8 ursprünglichen Thread ausführen 164 ursprünglicher Thread 160 UWP-Prozesse 796 VADs 458 W32PROCESS 117, 125 Zugriffstoken 784 Prozessoren Anzahl der Prozessoren pro Gruppe 307 Auswählen 320 idealer Prozesser 312 Knoten 58 KPCR 85 KPRCB 85 letzter Prozessor 312 NUMA 303 PCR 292 Planung 333 SMT 301 Status 308 symmetrisches Multiprocessing 57 zu Gruppen zuweisen 305 Prozessorsteuerregion (PCR) 292 Prozessreflexion 552 Prozessteuerungsblock (PCB) 118 Prozessumgebungsblock (PEB) 117 einrichten 159 Überblick 117 untersuchen 123 Prozess-VADs 458 PSOs siehe Kennworteinstellungsobjekte PspInsertProcess 117, 159 PTEs (Seitentabelleneinträge) Adressraum 404 Adressübersetzung 427 Definition 423 gemeinsame PTEs 530 Prototyp-PTEs 438 ungültige PTEs 436 Q Quantum 289 Konfiguration 265 Länge bestimmen 259 Registrierungswert 263 steuern 261 Taktintervall 258 Taktzyklen 260 Überblick 258 variable Quanten 262 R Randomisierung des Benutzeradressraums Abbilder 418 Heap 420 Stacks 420 Unterstützung 421 ReadyBoost 550 ReadyBoot 473 ReadyDrive 551 Index 909
Rechte 773 Rechteerweiterung mit Zustimmung 848 Rechteverwaltung siehe Active Directory-Rechteverwaltungsdienste Reflexion 552 Registrierung Sicherheitsschlüssel anzeigen 703 Threads 263 Überblick 36 Virtualisierung 838, 845 Reservierung Bandbreite 634 PFNs 508 reservierte Seiten 354 Ressourcen Ressourcenmonitor 40 Simulation geringer Ressourcen 641 Treiberüberprüfung 640 Ressourcenmonitor 40 Richtlinien Abwehrmaßnahmen auf Prozessebene 856 Authentifizierungsrichtlinien 710 Credential Guard 710 erweiterte Überwachungsrichtlinie 792 globale Überwachungsrichtlinie 790 Softwareeinschränkungsrichtlinien 882, 887 Trustlets 138 Windows-Editionen 63 RODC siehe Schreibgeschützte Domänencontroller Ruhezustand 546 S Sandbox 149 SAS 826 Sättigungswerte 241 Scatter/Gather-E/A 589 Scheduler 308 Schlanke Threads 21 Schnelle E/A 587 Schneller Benutzerwechsel 33, 546 Schreibbit 428 Schreibthread für geänderte Seiten 502 Schreibthread für zugeordnete Seiten 502 SDK (Software Development Kit) 48 Secure Attention Sequence 826 Secure-System-Prozess 101 Segmentheaps 383 Seiten freigeben 533 Speicherzusammenführung 530 Seitencluster 441 Seitenfehler Auslagerungsdatei 442 Auslagerungsdateien untersuchen 445 Einlagerungs-E/A 440 Größe der Auslagerungsdatei 452 910 Index Prototyp-PTEs 438 Seitencluster 441 Seitenfehlerkollisionen 440 Überblick 435 ungültige PTEs 436 UWP-Auslagerungsdatei 447 virtuelle Auslagerungsdatei 448 weiche Seitenfehler 437 Zusicherungsgrenzwert 448 Seitenfehlerkollisionen 440 Seitenframenummer siehe PFN Seitenheap 391 Seitenlisten 491 Seitentabellen 426 Seitentabelleneinträge siehe PTEs Seitenverzeichniseintrag (PDE) 425 Seitenverzeichnis-Zeigereintrag (PDPE) 425 Seitenverzeichnis-Zeigertabelle (PDPT) 424 Sichere Authentifizierung 833 Sichere Kommunikation 708 Sichere Prozesse Attribute 139 Dienste 141 identifizieren 143 Identität 140 integriert 140 Richtlinien 138 Systemaufrufe 142 Überblick 69, 137 Sicherer DLL-Suchmodus 179 Sicherer Kernel 67 Sicherheit Abwehrmaßnahmen 421 AppID 880 AppLocker 881 auslassen der durchsuchenden Überprüfung 782 AuthZ 771 CBAC 772 DAC 770 Dateivirtualisierung 839 Device Guard 712 grafische Sicherheitseditoren 768 Heap 388 Hypervisor 31 IBAC 771 Komponenten 700 Objekte 714 Objektzugriffsprüfung 716 Privilegien 773 Rechte 773 Registrierungsschlüssel 703 Registrierungsviertualisierung 839, 845 Secure-System-Prozess 101 sichere Kommunikation 708 sicherer Kernel 67 sichere Systemaufrufe 80
Sicherheitsdeskriptoren 751, 755 SIDs 720 SRPs 882, 887 Superprivilegien 783 Überblick 34, 697 UIPI 763 VBS 66 Virtualisierung 704, 838 virtuelle Dienstkonten 745 Sicherheitsaufruf 826 Sicherheitseinstufungen CC 699 TCSEC 697 Sicherheitskennungen siehe SIDs SIDs Besitzerrechte-SIDs 765 eingeschränkte Token 743 gefilterte Administratortoken 744 Identitätswechsel 741 Integritätsebenen 724, 740 Token 728 Überblick 720 untersuchen 722 verbindliche Beschriftung 726 Vertrauens-SIDs 760 Silos Container 211 erstellen 210 Hilfsfunktionen 211 Isolierung 205 Kontext 207 Objekte 204 Überblick 204 Überwachung 209 Siloüberwachung 209 Simulation geringer Ressourcen 641 Single Sign-On siehe Einmalige Anmeldung Sitzungen anzeigen 402 mehrere 32 Sitzungs-Manager 102 x86-Sitzungsraum 401 Sitzungs-Manager 102 Skalierbarkeit Betriebssystem 60 Scheduler 308 SMT 301 Software Development Kit (SDK) 48 Softwareeinschränkungsrichtlinien 882, 887 Software-PTE 437 Speicherkomprimierung 102 Speicherkomprimireung 515 Speicher-Manager AWE 367 Datenausführungsverhinderung 362 Dienste 350 freie Seiten 352 gemeinsam genutzter Speicher 358 gemeinsam nutzbare Seiten 352 gesamter zugesicherter Speicher 356 Granularität 357 große Seiten 344 im Speicher zugeordnete Dateien 358 interne Synchronisierung 350 kleine Seiten 343 Komponenten 342 kopieren beim Schreiben 366 NX-Seitenschutz 362 prozessübergreifender Speicherzugriff 352 reservierte Seiten 354 Seiten 344 Speicherschutz 360 Speicher untersuchen 346 Speicher verriegeln 356 Speicher zuweisen 352 Überblick 341 verzögerte Auswertung 367 zugesicherte Seiten 352 Zusicherungsgrenzwert 356 Speicherschutz 360 Speicherzusammenführung Freigabe von zusammengeführten Seiten 533 gemeinsame PTEs 530 Klassifizierung 529 Seiten zusammenführen 530 suchen 528 Überblick 526 untersuchen 534 Sperren Arbeitsspeicher 356 Prioritätserhöhung 269 Spezieller Pool 639 Spiele 282 SRPs 882, 887 Stabile Leistung 549 Stacks Benutzeradressraum 420 Benutzerstacks 455 DPC-Stack 457 Gerätestacks 649 IRPs 591, 599 Kernelstacks 456 Plug & Play 649 Randomisierung 420 Überblick 454 Standby Seitenlisten 494 SuperFetch 545 Starten von Teilsystemen 71 Status Prozessoren 308 Seiten 487 Threads 248 Index 911
Suche Abbildlader 178 sicherer DLL-Suchmodus 179 Speicherzusammenführung 527 SuperFetch Ablaufverfolgung 543 Komponenten 543 Protokollierung 544 Prozessreflexion 552 ReadyBoost 550 ReadyDrive 551 Ruhezustand 546 schneller Benutzerwechsel 546 Seitenprioritäten 546 stabile Leistung 549 Standby 545 Szenarien 545 Überblick 542 Superprivilegien 783 Swapper 482 Symbole Kerbeldebugging 42 Kerneltypinformationen 45 Symmetrisches Multiprocessing 57 Synchrone E/A 586 Synchronisierung Arbeitsspeicher 350 Heap 380 interne Synchronisierung 350 IRPs 610 Sysinternals 49 Systemadressraumlayout 401 Systemarbeitssätze 483 Systemaufrufe 142 Systemdienste 81 Systemfähigkeiten 686 System (Prozess) 100 Systemprozesse Dienstprozesse 109 Dienststeuerungs-Manager 107 Leerlaufprozess 100 Secure System 101 Sitzungs-Manager 102 Speicherkomprimierung 102 System (Prozess) 100 Systemspeicher und komprimierter Speicher 100 Systemthread 100 Überblick 98 Windows-Anmeldeprozess 109 Windows-Initialisierungsprozess 105 System-PTEs Adressraum 404 Adressübersetzung 427 Definition 423 912 Index gemeinsame PTEs 530 Prototyp-PTEs 438 ungültige PTEs 436 Systemspeicher und komprimierter Speicher 100 Systemthread 100 T Taktintervall 258 Taktzyklen 260 Task-Manager 9 TCB Mindest-TCB-Liste 131 Überblick 218 TCSEC 697 TEB Überblick 216, 220 untersuchen 224 Teilsysteme DLLs 70 Initialisierung 162 Konsolenhost 75 Linux 76 native Abbilder 81 Ntdll.dll 78 Pico 76 starten 71 Teilsystem-DLLs 54 Typen 70 Überblick 70 Windows-Teilsystem 72 Terminaldienste 32 Testbuild 64 Threadagnostische E/A 616 Threadinformationsblock (TIB) 223 Threadpools 335 Threads anhalten 296 Arbeitsfactorys 335 Ausgleichssatz-Manager 276 aushungern der CPU 276 auswählen 299, 319 Autoboost 286 beenden 292, 620 Benutzermodus 233 bereit 256 CSR_THREAD 217, 229 Dateizuordnungsobjekte 22 Deadline Scheduling 285 DFSS 326 Direct Switch 287 direkter Kontextwechsel 22 Dispatcher 240 Dispatcherdatenbank 255 Dispatchereignisse 267
dynamische Prozessoren 333 E/A 269 E/A abbrechen 619 Echtzeit 243 einfrieren 296 erstellen 215, 230, 455 ETHREAD 216 Exekutivressourcen 271 Fibers 21 freiwilliger Wechsel 288 geschützte Prozesse 237 Grenzwerte der CPU-Rate 330 Gruppenplanung 324 GUI-Threads 274 heterogen 322 Kerne 59 Kernelmodusdebugger 235 Kontext 20 Kontextwechsel 240, 286 KTHREAD 216 Leerlaufthreads 292, 301 maximale Anzahl 455 MMCSS 266, 282 Multimedia 282 Parallelität 623 PCR 292 Planungskategorie 283 Prioritäten untersuchen 245 Prioritätserhöhung 266 Prioritätserhöhungen entfernen 280 Prioritätsschübe 279 Prioritätsstufen 240 Prioritätsumkehr 276 Quantum 289 Sättigungswerte 241 sperren 269 Spiele 282 Status 248 Systemthread 100 TEB 223 Threadagnostische E/A 616 Threadpools 335 TIB 223 TLS 20 Überblick 20 UMS-Thread 21 untersuchen 231 Unwait-Erhöhungen 268 ursprünglicher Thread 160, 164 VADs 23 Vordergrundthreads 271 Vorzeitig unterbrechen 289 Zeitplanung 238 Zugriffstoken 22, 784 Threadsteuerungsblock (TCB) Mindest-TCB-Liste 131 Überblick 218 Threadumgebungsblock (TEB) Überblick 216, 220 untersuchen 224 TIB 223 TLB 429 TLS 20 Token Anwendungscontainer 801 BNO-Isolierung 822 gespeicherte Handles 819 SIDs 728 Zugriffstoken 784 Tools Debugging Tools for Windows 43 Windows 39 Treiber siehe Gerätetreiber Treiberobjekte 574 Treiberüberprüfung E/A 638 IRQL-Überprüfung 641 Poolnachverfolgung 640 Simulation geringer Ressourcen 641 sonstige Prüfungen 642 spezieller Pool 639 Überblick 635 Trusted Computer System Evaluation Criteria (TCSEC) 697 Trustlets Attribute 139 Dienste 141 identifizieren 143 Identität 140 integriert 140 Richtlinien 138 Systemaufrufe 142 Überblick 69, 137 Typen Gerätetreiber 564 Heap 380 Kernel 45 Teilsysteme 70 U UAC (User Account Control) siehe Benutzerkontensteuerung Über-die-Schulter-Rechteerweiterung 848 Überprüfter Build 64 Überwachung erweiterte Überwachungsrichtlinie 792 globale Überwachungsrichtlinie 790 Objektzugriffsüberwachung 787 Überblick 784 UEFI 710 UIPI (User Interface Privilege Isolation) 763 UMDF 565, 664, 667 Index 913
Umfangreiche Adressräume 399 Umgebung Atomtabelle 808 Sicherheitsattribute 806 Überblick 803 Umlauf-VADs 460 UMS-Threads 21 Ungültige PTEs 436 Unicode 37 Universelle Windows-Treiber 95 Untersuchen Abbildlader 175 Ablaufsteuerungsschutz 863 Abschnittsobjekte 463 Adressübersetzung 430 Adressverwendung 410 aktive Anmeldesitzungen 830 Anwendungscontainer 805 Anwendungscontainertoken 801 Arbeitssätze 478 Arbeitsspeicher 345 Atomtabelle 808 ausführungsbereite Threads 256 aushungern der CPU 277 Auslagerungsdatei 443, 452 Benachrichtigungsereignisse 486 Benutzeradressraum 416, 421 Benutzermodusdebugger 233 Broker 822 CFG-Bitmap 868 CSR_PROCESS 124 CSR_THREAD 229 Dateiobjekte 583 Datenausführungsverhinderung 365 Datenbank der geladenen Module 185 Datenstrukturen 119 DFSS (Dynamic Fair Share Scheduling) 328 Dienste 108 Dienstprozesse 109 Dispatchroutinen 594 DLL-Suchreihenfolge 182 Durchsatz 632 Energiefähigkeiten 686 Energieverfügbarkeitsanforderungen 693 Energiezuordnungen 685 Energiezustände 681 Energiezustandszuordnung 685 EPROCESS 119 ETHREAD 218 exportierte Funktionen 38 Fähigkeiten von Anwendungscontainern 813 gefilterte Administratortoken 744 Geräteknoten 653 Geräteobjekte 576 Gerätetreiber 95 geschützte Prozesse 132, 237 globale Überwachungsrichtlinie 790 914 Index GUI-Threads 274 HAL-Abbildabhängigkeiten 90 Heap 384 im Speicher zugeordnete Dateien 359 INF-Dateien 658 Integritätsebenen 724 in Token gespeicherte Handles 819 IRPs 595, 599 Jobs 200 Katalogdateien 660 Kernelmodusdebugger 235 Kernelstacks 456 KMDF-Treiber 666 KPCR 86 KPRCB 86 KTHREAD 218 Leerlaufthreads 292 Lese- und Schreibvorgänge in der PrefetchDatei 475 Liste der freien Seiten 492 Liste geänderter Seiten 494 Look-Aside-Listen 377 MMCSS (Multimedia Class Scheduler Service) 284 Nullseitenlisten 493 NUMA-Prozessoren 303 Objektzugriffsüberwachung 787 PEB 122 PFN 490, 508 Pools 337 Prioritäten 244, 501 Prioritätserhöhungen und -bumps 633 Prioritätsschübe und -bumps 633 Privilegien aktivieren 781 Process Explorer 16, 132 Prozessaffinität 309 Prozessbaum 13 PTEs (Seitentabelleneinträge) 404 Registrierungsschlüssel 703 schnelle E/A 587 Seitenheap 391 Sicherheitsattribute von Anwendungscontainern 806 Sicherheitsdeskriptoren 755 SIDs 722 Sitzungen 401, 830 SMT-Prozessoren 302 Speicherkomprimierung 522 Speicherpartitionen 525 Speicherzusammenführung 534 SRPs 889 Stacks 456, 600 Standby-Seitenlisten 494 Steuerbereiche 467 Systemdienstdispatcher 79 Systemfähigkeiten 686 Taktintervall 258
Taktzyklen pro Quantum 260 Task-Manager 9 Teilsysteme 70 Threads 231 Threads einfrieren 297 Threadstatus 250 Token 732 Treiber 568 Treiberobjekte 579 Typinformationen 45 UMDF-Treiber 667 Unterstützung für Randomisierung 421 UWP-Auslagerungsdatei 448 UWP-Prozesse 799 VADs 458 Vertrauens-SIDs 760 Virtualisierung 843 virtuelle Auslagerungsdatei 448 Vordergrundthreads 271 Windows-Funktionen 63 Windows-Tools 39 Zugriffsmaske 719 Unwait-Erhöhungen 268 UPNs siehe Benutzerprinzipalnamen Ursprünglicher Thread ausführen 164 erstellen 160 User Interface Privilege Isolation (UIPI) 763 UWP 794 UWP-Auslagerungsdatei 447 V VADs Prozess-VADs 458 Überblick 23, 457 Umlauf-VADs 460 Variable Quanten 262 VBS Architektur 66 Hypervisor 31 Verbindliche Beschriftung 726 Verbundener Standbymodus Energieverfügbarkeitsanforderungen 692 Energieverwaltung 689 Energieverwaltungsframework 690 Energiezuordnungen 684 Energiezustände 679 Legacy-Standby 683 Leistungsstatus 691 moderner Standby 683 Systemfähigkeiten 686 Treiber 685 Überblick 683 Vererbung 758 Vertrauens-SIDs 760 Vervollständigungsports 622 Verzögerte Auswertung 367 Verzögerte Prozeduraufrufe E/A 563 Stacks 457 Virtualisierung Authentifizierungsrichtlinien 710 Dateien 838 Device Guard 712 Kennwörter 706 Kerberos-Armoring 710 NTOWF/TGT 707 Registrierung 838, 845 sichere Kommunikation 708 Überblick 704, 839 UEFI 710 Virtualisierungsgestützte Sicherheit siehe VBS Virtuelle Adressdeskriptoren siehe VADs Virtuelle Auslagerungsdatei 448 Virtuelle Dienstkonten 745 Virtueller Arbeitspeicher 23 Virtuelle Vertrauensebenen 66 Vordergrundthreads 271 Vorzeitige Unterbrechung 289 VSM siehe VBS VTLs (Virtual Trust Levels) 66 W W32PROCESS 117, 125 WBF (Windows Biometric Framework) 835 WDK (Windows Driver Kit) 49 KMDF 664 Überblick 665 UMDF 565, 664, 667 Weiche Seitenfehler 437 Windows aktivierte Features 63 aktualisieren 3 Betriebssystemmodell 52 Designziele 51 Editionen 61 interne Mechanismen untersuchen 39 OneCore 3 Portierbarkeit 56 SDK (Software Development Kit) 48 Speichergrenzwerte für Clienteditionen 513 Versionen 1 W32PROCESS 117, 125 WBF (Windows Biometric Framework) 835 WDK (Windows Driver Kit) 49 WDM (Windows Driver Model) 93, 566 Windows-Anmeldeprozess 109 Windows Hello 837 Windows-Initialisierungsprozess 105 Windows-Laufzeit 5 Index 915
Windows-Teilsystem 72 Winlogon-Initialisierung 826 winver 2 WSRM (Windows System Resource Manager) 248 Windows-Anmeldeprozess 109 Windows-Biometrieframework 835 Windows Driver Foundation (WDF) KMDF 664 Überblick 94, 664 UMDF 565, 664, 667 Windows Driver Kit (WDK) 49 Windows Driver Model (WDM) 93, 566 Windows Hello 837 Windows Identity Foundation siehe WIF Windows-Initialisierungsprozess 105 Windows-Laufzeit 5 Windows System Resource Manager (WSRM) 248 Winlogon-Initialisierung 826 WSRM (Windows System Resource Manager) 248 X x64 x86 Adressübersetzung 432 Einschränkungen für virtuelle x64-Adressen 408 Systeme 56 Adressraumlayout 397 Adressübersetzung 422 Sitzungsraum 401 Systemadressraumlayout 401 Z Zeitplanung Deadline Scheduling 285 DFSS 326 Dispatcher 240 dynamische Prozessoren 333 freiwilliger Wechsel 288 Grenzwerte der CPU-Rate 330 Gruppen 324 heterogene Threads 322 Kontextwechsel 240 Planungskategorie 283 Quantum 289 916 Index Scheduler 308 Threads beenden 292 Threads vorzeitig unterbrechen 289 Zugesicherte Seiten 352 ausgelagerter Pool 370 freie Seiten 352 gemeinsam nutzbare Seiten 352 PDE 425 PDPE 425 PDPT 424 Prioritäten 499, 546 reservierte Seiten 354 Schreibthread für geänderte Seiten 502 Schreibthread für zugeordnete Seiten 502 Seitenheap 391 Seitentabellen 426 Speicher-Manager 352 Status 487 SuperFetch 546 Zugriff bedingte Zugriffssteuerungseinträge 772 Besitzerrechte-SIDs 765 grafische Sicherheitseditoren für Zugriffssteuerungslisten 768 Objektzugriffsüberwachung 787 Vererbung von Zugriffssteuerungslisten 759 Vertrauens-SIDs 760 Zugriff anhand von Zugriffssteuerungslisten bestimmen 761 Zugriffsmaske 719 Zugriffsprüfung 716 Zugriffssteuerungseinträge 753 Zugriffssteuerungslisten 753 Zugriffssteuerungslisten zuweisen 758 Zugriffstoken 22, 784 Zusicherungen Betriebssystem 876 Compiler 876 Fehlercodes 877 Überblick 875 Zusicherungsgrenzwert gesamter zugesicherter Speicher 448 Speicher-Manager 356 Zuweisung Arbeitsspeicher 352 dynamische Zuweisung 408 Kontingente 415 Prozessoren zu Gruppen 305 Zugriffssteuerungslisten 758 Zwischenspeichern 589