Freigeben über


CLR-Debugarchitektur

Die Debug-API der Common Language Runtime (CLR) wurde so konzipiert, dass sie wie ein Teil des Betriebssystemkernels verwendet werden kann. Wenn ein Programm in nicht verwaltetem Code eine Ausnahme generiert, unterbricht der Kernel die Ausführung des Prozesses und übergibt die Ausnahmeinformationen mithilfe der Win32-Debug-API an den Debugger. Die Debug-API der CLR stellt die gleiche Funktionalität für verwalteten Code bereit. Wenn verwalteter Code eine Ausnahme generiert, unterbricht die Debug-API der CLR die Ausführung des Prozesses und übergibt die Ausnahmeinformationen an den Debugger.

In diesem Thema wird beschrieben, wie und wann die Debug-API der CLR einbezogen wird und welche Dienste sie bereitstellt.

Prozessarchitektur

Die Debug-API der CLR umfasst die folgenden beiden Hauptkomponenten:

  • Die Debug-DLL, die immer in den gleichen Prozess geladen wird wie das Programm, das gedebuggt wird. Der Laufzeitcontroller ist verantwortlich für die Kommunikation mit der CLR, die Ausführungskontrolle und die Überprüfung von Threads, die verwalteten Code ausführen.

  • Die Debuggerschnittstelle, die in einen anderen Prozess geladen wird als das Programm, das gedebuggt wird. Die Debuggerschnittstelle übernimmt für den Debugger die Kommunikation mit dem Laufzeitcontroller. Außerdem ist sie verantwortlich für die Behandlung von Win32-Debugereignissen, die aus dem gedebuggten Prozess stammen, und für deren Übergabe an einen Debugger für nicht verwalteten Code. Die Debuggerschnittstelle ist der einzige Teil der Debug-API der CLR, deren API verfügbar gemacht wurde.

Die CLR-Debug-API unterstützt keine computer- oder prozessübergreifende Remoteverwendung. Das heißt, ein Debugger, der die API verwendet, muss von innerhalb eines eigenen Prozesses auf die API zugreifen, wie im folgenden API-Architekturdiagramm gezeigt. Diese Abbildung zeigt, wo sich die verschiedenen Komponenten der Debug-API der CLR befinden und wie sie mit der CLR und dem Debugger interagieren.

Architektur der Debug-API der CLR

CLR-Debuggen-Architektur

Debugger für verwalteten Code

Es ist möglich, einen Debugger zu erstellen, der nur verwalteten Code unterstützt. Die Debug-API der CLR ermöglicht einem solchen Debugger, sich bei Bedarf mithilfe eines Mechanismus für weiches Anfügen an einen Prozess anzufügen. Ein Debugger, der auf diese Weise an einen Prozess angefügt ist, kann sich anschließend vom Prozess trennen.

Threadsynchronisierung

Die Debug-API der CLR hat widersprüchliche Anforderungen im Hinblick auf die Prozessarchitektur. Einerseits sprechen viele überzeugende Gründe dafür, dass die Debuglogik im gleichen Prozess bleiben sollte wie das Programm, das gedebuggt wird. Beispielsweise sind die Datenstrukturen komplex und werden anstelle eines festen Speicherlayouts häufig von Funktionen geändert. Es ist deutlich einfacher, die Funktionen direkt aufzurufen, als zu versuchen, die Datenstrukturen außerhalb des Prozesses zu decodieren. Für den Erhalt der Debuglogik im gleichen Prozess spricht auch, dass durch den Wegfall des Aufwands für die prozessübergreifende Kommunikation die Leistung verbessert wird. Ein wichtiges Merkmal des CLR-Debuggens ist nicht zuletzt die Möglichkeit, Benutzercode im gleichen Prozess auszuführen wie die zu debuggende Komponente, was selbstverständlich ein gewisses Maß an Kooperation mit dem zu debuggenden Prozess erfordert.

Andererseits muss das CLR-Debuggen neben dem Debuggen von nicht verwaltetem Code möglich sein, das wiederum nur von einem externen Prozess aus richtig durchgeführt werden kann. Außerdem ist ein prozessexterner Debugger sicherer als ein prozessinterner Debugger, da der Konflikt zwischen dem Debugvorgang und dem zu debuggenden Prozess in einem prozessexternen Debugger minimiert wird.

Aufgrund dieser widersprüchlichen Anforderungen kombiniert die Debug-API der CLR Teile beider Ansätze. Die primäre Debugschnittstelle ist prozessextern und existiert neben den systemeigenen Win32-Debugdiensten. Die Debug-API der CLR bietet darüber hinaus jedoch die Möglichkeit der Synchronisierung mit dem zu debuggenden Prozess, sodass Code im Benutzerprozess sicher ausgeführt werden kann. Für diese Synchronisierung interagiert die API mit dem Betriebssystem und der CLR, um alle Threads im Prozess an einer Stelle anzuhalten, an der sie keinen Vorgang unterbrechen und die Laufzeit in keinem inkohärenten Zustand lassen. Anschließend kann der Debugger Code in einem speziellen Thread ausführen, der den Zustand der Laufzeit überprüfen und ggf. Benutzercode aufrufen kann.

Wenn verwalteter Code eine Haltepunktanweisung ausführt oder eine Ausnahme generiert, wird der Laufzeitcontroller benachrichtigt. Diese Komponente bestimmt, welche Threads verwalteten Code ausführen und welche Threads nicht verwalteten Code ausführen. Normalerweise können Threads, die verwalteten Code ausführen, die Ausführung fortsetzen, bis sie einen Zustand erreichen, in dem sie sicher angehalten werden können. Beispielsweise müssen sie eine Garbage Collection, die gerade ausgeführt wird, beenden. Wenn die Threads, die verwalteten Code ausführen, einen sicheren Zustand erreicht haben, werden sie angehalten. Daraufhin informiert die Debuggerschnittstelle den Debugger, dass ein Haltepunkt oder eine Ausnahme empfangen wurde.

Wenn nicht verwalteter Code eine Haltepunktanweisung ausführt oder eine Ausnahme generiert, empfängt die Debuggerschnittstellenkomponente über die Win32-Debug-API eine entsprechende Benachrichtigung. Diese Benachrichtigung wird an einen nicht verwalteten Debugger übergeben. Wenn der Debugger eine Synchronisierung durchführen möchte (damit z. B. Stapelrahmen für verwalteten Code überprüft werden können), muss die Debuggerschnittstelle den angehaltenen zu debuggenden Prozess zunächst neu starten und dann den Laufzeitcontroller anweisen, die Synchronisierung durchzuführen. Die Debuggerschnittstelle wird benachrichtigt, sobald die Synchronisierung abgeschlossen wurde. Diese Synchronisierung ist für den nicht verwalteten Debugger transparent.

Der Thread, der die Haltepunktanweisung oder die Ausnahme generiert hat, darf während der Synchronisierung nicht ausgeführt werden. Um dies zu vereinfachen, kontrolliert die Debuggerschnittstelle den Thread, indem der Filterkette des Threads ein spezieller Ausnahmefilter hinzugefügt wird. Wenn der Thread neu gestartet wird, gelangt er zum Ausnahmefilter und wird so der Kontrolle des Laufzeitcontrollers unterstellt. Wenn die Ausnahmeverarbeitung fortgesetzt (oder die Ausnahme abgebrochen) werden muss, gibt der Filter die Kontrolle an die reguläre Ausnahmefilterkette des Threads oder das richtige Ergebnis zur Fortsetzung der Ausführung zurück.

In seltenen Fällen verfügt der Thread, der die systemeigene Ausnahme generiert hat, über wichtige Sperren, die aufgehoben werden müssen, bevor die Synchronisierung der Laufzeit abgeschlossen werden kann. (Normalerweise handelt es sich dabei um Bibliothekssperren auf niedriger Ebene, z. B. Sperren auf dem malloc-Heap.) In solchen Fällen muss die Synchronisierung unterbrochen werden und schlägt fehl. Dadurch schlagen bestimmte Vorgänge, die eine Synchronisierung erfordern, ebenfalls fehl.

Der prozessinterne Hilfsthread

In jedem CLR-Prozess wird ein Hilfsthread des Debuggers verwendet, um sicherzustellen, dass die Debug-API der CLR ordnungsgemäß funktioniert. Dieser Hilfsthread ist für viele Überprüfungsdienste, die von der Debug-API bereitgestellt werden, und unter bestimmten Umständen für die Threadsynchronisierung verantwortlich. Sie können die ICorDebugProcess::GetHelperThreadID-Methode verwenden, um den Hilfsthread zu identifizieren.

Interaktionen mit JIT-Compilern

Um einem Debugger das Debuggen von Just-In-Time (JIT)-kompiliertem Code zu ermöglichen, muss die Debug-API der CLR Informationen aus der Microsoft Intermediate Language (MSIL)-Version einer Funktion der systemeigenen Version der Funktion zuordnen können. Zu diesen Informationen zählen auch solche über Sequenzpunkte im Code und über Speicherorte von lokalen Variablen. In .NET Framework, Version 1.0 und 1.1, wurden diese Informationen nur erzeugt, wenn sich die Laufzeit im Debugmodus befand. In .NET Framework 2.0 werden diese Informationen laufend erzeugt.

JIT-kompilierter Code kann auch stark optimiert werden. Optimierungen wie das Entfernen gemeinsamer Teilausdrücke, die Inlineerweiterung von Funktionen, das Entladen von Schleifen, das Verschieben von Code usw. können zu einem Korrelationsverlust zwischen dem MSIL-Code einer Funktion und dem systemeigenen Code, der für deren Ausführung aufgerufen wird, führen. Demnach wird die Möglichkeit des JIT-Compilers, richtige Zuordnungsinformationen bereitzustellen, durch diese aggressiven Codeoptimierungsverfahren stark beeinträchtigt. Aus diesem Grund führt der JIT-Compiler bestimmte Optimierungen nicht aus, wenn sich die Laufzeit im Debugmodus befindet. Diese Einschränkung ermöglicht Debuggern die genaue Bestimmung der Quellzeilenzuordnung und des Speicherorts aller lokalen Variablen und Argumente.

Debugmodi

Die Debug-API der CLR stellt zwei spezielle Modi zum Debuggen bereit:

  • Modus Bearbeiten und Fortfahren. In diesem Fall funktioniert die Laufzeit anders, damit Code später geändert werden kann. Der Grund dafür ist, dass das Layout bestimmter Laufzeitdatenstrukturen zur Unterstützung von Bearbeiten und Fortfahren unterschiedlich sein muss. Da sich dies nachteilig auf die Leistung auswirkt, sollten Sie diesen Modus nur verwenden, wenn Sie seine Funktionalität benötigen.

  • Debugmodus Dieser Modus ermöglicht dem JIT-Compiler, Optimierungen auszulassen. Daher ist die Ausführung von systemeigenem Code enger mit der Hochsprachenquelle verbunden. Da sich dieser Modus ebenfalls nachteilig auf die Leistung auswirkt, sollten Sie ihn nur verwenden, wenn dies unbedingt erforderlich ist.

Wenn Sie ein Programm außerhalb des Modus Bearbeiten und Fortfahren debuggen, wird dessen Funktionalität nicht unterstützt. Wenn Sie ein Programm außerhalb des Debugmodus debuggen, werden die meisten Debugfunktionen zwar unterstützt, Optimierungen können jedoch zu ungewöhnlichem Verhalten führen. Beispielsweise kann es sein, dass bei der Einzelschrittausführung scheinbar willkürliche Sprünge zwischen den Zeilen in der Methode erfolgen und dass Inlinemethoden in einer Stapelüberwachung nicht angezeigt werden.

Ein Debugger kann sowohl den Modus Bearbeiten und Fortfahren als auch den Debugmodus programmgesteuert über die Debug-API der CLR aktivieren, wenn der Debugger die Kontrolle über einen Prozess erlangt, bevor sich die Laufzeit selbst initialisiert hat. Dies ist für viele Zwecke ausreichend. Ein Debugger, der sich an einen Prozess anfügt, welcher schon seit einer Weile ausgeführt wird (z. B. beim JIT-Debuggen), kann diese Methoden nicht starten.

Um diesen Problemen zu begegnen, kann ein Programm im JIT-Modus oder Debugmodus unabhängig von einem Debugger ausgeführt werden. Informationen über Möglichkeiten zum Aktivieren des Debuggens finden Sie unter Debuggen, Ablaufverfolgung und Profilerstellung.

JIT-Optimierungen können eine Anwendung weniger debugfähig machen. Die Debug-API der CLR ermöglicht die Überprüfung von Stapelrahmen und lokalen Variablen mit JIT-kompiliertem Code, der optimiert wurde. Die Schrittausführung wird zwar unterstützt, kann aber u. U. ungenau sein. Sie können ein Programm ausführen, das den JIT-Compiler anweist, alle JIT-Optimierungen zu deaktivieren, um debugfähigen Code zu erzeugen. Ausführliche Informationen finden Sie unter Erleichtern des Debuggens für ein Abbild.

Siehe auch

Konzepte

Übersicht über das Debugging in der CLR

Weitere Ressourcen

Debuggen (Referenz zur nicht verwalteten API)