Glengamoi (Forum) · AspHeute · .NET Heute (RSS-Suche) · AspxFiles (Wiki) · .NET Blogs

Debugging in der Tiefe

Geschrieben von: Bernhard Spuida
Kategorie: Aprilscherz

This printed page brought to you by AlphaSierraPapa

Im heutigen Artikel beschäftige ich mich mit Debugging von User Stacks. Debugging ist auch mit dem Debug Interface von C# nach wie vor eher eine Kunstform als eine exakte Wissenschaft. Besonders der Zugriff auf tiefliegende Komponenten eines Programmes ist schwierig, wenn man Seiteneffekte zur Laufzeit vermeiden will.

Das Debugging Interface des .NET SDK Framework ist gut dokumentiert und kann durch Dritte in seiner Funktionalität erweitert werden. Die Debugging-Funktionalität liegt in den Systems.Diagnostics Klassen. Am Beispiel der User Stacks zeige ich, wie eine Erweiterung der Debugging-Funktionalität implementiert werden kann.

Voraussetzung für das bessere Verständnis des Artikels ist eine genaue Kenntnis des julianischen Kalenders in Bezug auf datumsmäßig bedingte Bräuche.

User Stacks

Zuerst einmal möchte ich den Begriff User Stack erklären: Ein Programm verwendet zur Verwaltung seiner Daten und Speicherstrukturen den Heap einerseits und den Stack andererseits. Der wesentliche Unterschied dieser beiden Strukturen ist die Handhabung von Ein- und Ausgabe von Daten - am Stack wird immer das zuletzt abgelegte Element zurückgegeben. Es handelt sich also um einen LIFO (last in, first out) Puffer. In Wirklichkeit ist wie zu erwarten die Verwendung des Stacks nicht ganz so einfach. Es gibt de facto mehrere Stacks, die unterschiedliche Aufgaben wahrnehmen: Event Queueing, I/O-Pufferung etc. Die blutigen Details lasse ich aus, um den geneigten Leser vor dem Einschlafen zu bewahren.

Richtig interessant wird es sobald wir als Entwickler unsere eigenen Stacks anlegen - das geht und ist nützlich! User Stacks können wir zum Beispiel verwenden um in einer Web-Applikation Nachrichten in einem Chat zu verwalten, Warteschlangen einzurichten etc. Die Verwendung von User Stacks nimmt uns viel Arbeit bei der Verwaltung dieser Puffer ab.

Debugging systemnaher Datenstrukturen bringt immer Probleme mit sich:

Speziell im Falle von User Stacks besteht die Gefahr daß ein einfacher, naiver Direktzugriff auf den Stack die Integrität des Stacks gefährdet. Lesen von einem Stack schließt im Normalfall einen pop ein, also das Entfernen des Wertes vom Stack. Das ist beim Debuggen genau das was wir nicht wollen - der Debugvorgang beeinflußt den Zustand des zu debuggenden Programms. Schreibt man das Element wieder zurück, hat man im günstigsten Falle eine Performance-Einbuße, im schlimmsten Fall ein 'zerschossenes' Programm, falls in der Zwischenzeit ein Stackzugriff erfolgte. Da die System.Diagnostics Klassen wie alle C# Klassen erweiterbar sind, gibt es jedoch Lösungsmöglichkeiten.

Und genau einen solchen Ansatz stellt die Klasse Debug.Sub dar. Sub steht dabei für Stack Use Buffer. Die Klasse Debug.Sub setzt auf sogenannte OHIOs - OverHead Independent Objects - auf. Dadurch werden die mit Debugvorgängen verbundenen Performanceprobleme minimiert. Die Integritätsproblematik wird dadurch gelöst, daß statt den User Stack auf normalen Arrays abzubilden, ein spezieller Array-Typ zum Einsatz kommt: das TowedArray.

Die grundlegende Idee hinter TowedArrays ist die, daß im Gegensatz zu normalen Arrays von unserer Seite aus keine schreibenden Zugriffe erfolgen können. Durch diese Pufferung des User Stacks wird selbst wenn man einen Weg finden sollte ihn zu schreiben, die Integrität des User Stacks gewährleistet. Beschickt wird das Array über eine Kopie des zu beobachtenden Stacks. Der Name TowedArray ergibt sich also aus diesem 'Nachschleppen' hinter dem eigentlichen Stack.

Jedes Towed Array kann mehrere sogenannte Streamer beinhalten, die jeweils einen User Stack überwachen. Die Überwachung kann in zwei verschiedenen Modi erfolgen: listen und pulse. Der listen-Modus liefert ein kontinuierliches Update des Stack Use Buffers, der pulse-Modus bildet den User Stack auf einen Trigger hin als 'Snapshot' ab.

Die Debug.Sub Klasse

Im Folgenden eine rudimentäre Implementation der Debug.Sub Klasse (Listing). Wie man sieht, ist es in C# ein Leichtes, leistungsfähige Erweiterungen der Debugfunktionalität zu implementieren. Diese zusätzliche Funktionalität steht natürlich auch in allen anderen .NET Sprachen zur Verfügung.

// Sub class :
        class sealed Debug.Sub {

                // ...

                // overloaded  {...} operators :

                public Streamer this{int trace} {
                        get;
                }
                public Streamer this{int trace, int section} {
                        get;
                }

                /// <summary>
                /// Attaches a streamer to a user stack.
                /// </summary>
                public void Lower(UserStack stack);

                /// <summary>
                /// Detaches a streamer from a user stack.
                /// </summary>
                public void Raise(UserStack stack);

                /// <summary>
                /// Sets the towed array to listen mode.
                /// </summary>
                public void Silent();

                /// <summary>
                /// Sets the towed array to pulse mode.
                /// </summary>
                public void Pulse();

                /// <summary>
                /// Gets a current 'snapshot' in pulse mode.
                /// </summary>
                /// <Exception name="SinkException">Is thrown when the towed
                /// array is in listen mode and <code>Trigger()</code> is called.
                /// </Exception>
                public void Trigger();

                public event TowedArrayEventHandler TowedArrayEvent;
        }

Bemerkenswert an diesem Stück Code ist die Überladung von Streamer, da wir es hier mit TowedArrays statt gewöhnlichen Arrays zu tun haben. Der Triggermechanismus wird wie man sieht über eine Exception gehandhabt. Dieses Vorgehen dient zur weiteren Minimierung des Overheads des Debugvorganges, da Exceptions vom System abgehandelt werden und somit unseren Userprozess nicht belasten. Es ist auch wichtig zu beachten, daß nicht nur das gesamte TowedArray, sondern auch einzelne Streamer gezielt abgefragt werden können. Ebenso ist es vorgesehen, die Streamer individuell in das TowedArray aufzunehmen und wieder auszugliedern, da nicht jeder User Stack die gesamte Lebensdauer des Programmes über existiert.

Anwendung

Nun eine einfache Beispielanwendung von Debug.Sub, die beide Modi demonstriert:

using System;
using System.Diagnostics;

class OhioSample
{
        Debug.Sub myTowedArray;

        public void OhioSample()
        {
                myTowedArray = new Debug.Sub();
        }

        public void RunPulseSample()
        {
                // demonstrate pulse
                myTowedArray.Pulse();
                // attaches the towed array to the user stack
                myTowedArray.Lower(Debug.UserStack);
                for (int j = 0; j < 5; ++j) {
                        // get 'snapshot'
                        myTowedArray.Trigger();
                        // shows the streamer as string data.
                        for (int i = 0; i < myTowedArray.Length;++i) {
                                Console.WriteLine(myTowedArray{i}.ToString());
                        }
                }
                // detaches the towed array from the user stack
                myTowedArray.Raise(Debug.UserStack);
        }

        void TowedEventListener(object sender, TowedArrayEventArgs e)
        {
                Console.WriteLine("Tracked  call : " + e.TowedArray{e.RetrievedTrace}{e.RetrievedSection});
        }

        public void RunListenSample()
        {
                // demonstrate silent
                myTowedArray.Silent();
                myTowedArray.TowedArrayEvent += new TowedArrayEventHandler(TowedEventListener);

                // attaches the towed array to the user stack
                myTowedArray.Lower(Debug.UserStack);

                // track 10 Console.WriteLine calls
                for (int i = 0; i < 10;++i) {
                        Console.WriteLine("I'm call " + i);
                }

                // detaches the towed array from the user stack
                myTowedArray.Raise(Debug.UserStack);
        }

        public static void Main(string[] args)
        {
                OhioSample sample = new OhioSample();
                sample.RunPulseSample();
                sample.RunListenSample();
        }
}

Sehen wir uns diesen Code etwas näher an: zuerst werden die benötigten Namespaces System und System.Diagnostics importiert, gefolgt von der Erzeugung einer Instanz der Klasse OhioSample - und dort wird auch das TowedArray eingerichtet:

class OhioSample
{
        Debug.Sub myTowedArray;

        public void OhioSample()
        {
                myTowedArray = new Debug.Sub();
        }
        ...

Ist doch einfach, oder? Ab jetzt steht uns das TowedArray zur Verfügung. Die Instrumentierung von C#-Code für erfolgreiches Debugging ist auch nicht aufwendiger als Einstreuen von diagnostischen Console.WriteLine Statements. Und wir können noch dazu viel tiefer in unseren Code eintauchen...

Die eigentliche Verfolgung der User Stacks erfolgt in sehr ähnlichen Routinen, so daß hier nur RunPulseSample() betrachtet wird:

 public void RunPulseSample()
        {
                // demonstrate pulse
                myTowedArray.Pulse();
                // attaches the towed array to the user stack
                myTowedArray.Lower(Debug.UserStack);
                for (int j = 0; j < 5; ++j) {
                        // get 'snapshot'
                        myTowedArray.Trigger();
                        // shows the streamer as string data.
                        for (int i = 0; i < myTowedArray.Length;++i) {
                                Console.WriteLine(myTowedArray{i}.ToString());
                        }
                }
                // detaches the towed array from the user stack
                myTowedArray.Raise(Debug.UserStack);
        }

Die simple Eleganz der Routine macht eine Erklärung nahezu überflüssig. Nach dem Aktivieren durch myTowedArray.Pulse() wird das Array mit dem zu überwachenden User Stack Debug.Userstack verbunden. Unser TowedArray besitzt also nur einen Streamer. Anschließend werden 5 Snapshots gemacht und ausgegeben. Zu guter Letzt wird das TowedArray wieder frei gegeben. Das war's! Wozu User Stacks 'von Hand' zu debuggen versuchen, wenn es mit TowedArrays so einfach geht?

Schlußbemerkung

Es ist nicht allzu schwierig, sich erweiterte Debug-Funktionalität in .NET zu verschaffen. Gleichzeitig kann man wie gezeigt die üblichen Probleme mit Low-Level Debugging vermeiden. May the source be with you!

This printed page brought to you by AlphaSierraPapa

Download des Codes

Klicken Sie hier, um den Download zu starten.
http://www.aspheute.com/code/20020401.zip

Verwandte Artikel

Arrays mit Index und Schlüssel
http:/www.aspheute.com/artikel/20020124.htm
Collections einmal anders: Stacks und Queues
http:/www.aspheute.com/artikel/20000926.htm
Die Hashtable Klasse
http:/www.aspheute.com/artikel/20000823.htm
Hochleistungskompression mit .NET-Bordmitteln
http:/www.aspheute.com/artikel/20030401.htm
Neues in .NET Codename "Swinomish"
http:/www.aspheute.com/artikel/20040401.htm
Schreib"kunst" für Programmierer, Teil I
http:/www.aspheute.com/artikel/20020506.htm
Verwendung von Arrays in C#
http:/www.aspheute.com/artikel/20000731.htm

Links zu anderen Sites

Patrick Robinson - USS Seawolf
http://www.patrickrobinson.com/books/seawolf.html
The History of Sonar
http://inventors.about.com/library/inventors/blsonar.htm
The Hunt for Red October
http://www.kerzap.com/hfro/
U.S. Navy - Submarine Images
http://www.chinfo.navy.mil/navpalib/images/imagesub.html

 

©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.