(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

ILDASM ist Ihr bester Freund

Veröffentlicht: 18. Nov 2001 | Aktualisiert: 18. Jun 2004

Von John Robbins

In diesem Artikel möchte ich Ihnen die wichtigsten MSIL-Befehle vorstellen und Ihnen einige Konstrukte zeigen, damit Sie im .NET schneller Fahrt aufnehmen können. Bevor ich aber auf die Befehle zu sprechen komme, möchte ich kurz beschreiben, was Sie in diesen Textfenstern sehen werden, die bei der Arbeit mit dem ILDASM überall aufspringen.

Auf dieser Seite

ILDASM-Grundlagen
CLR-Grundlagen
MSIL, Parameter und lokale Variablen
Die wichtigen Befehle
Fazit

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

sys


In der .NET-Welt ist der kleinste gemeinsame Nenner aller Sprachen die Zwischensprache MSIL, für die es auch einen Disassembler namens ILDASM gibt. Durch das Verständnis dieser "Assemblersprache für .NET" gewinnen Sie einen nützlichen Einblick in die interne Funktionsweise des Systems.

Der Rummel um das Microsoft .NET hat Sie inzwischen vermutlich zumindest dazu gebracht, die Beta 1 vom Visual Studio.NET zu installieren und sich das neue Ungetüm einmal anzusehen. Anfangs dachten Sie vermutlich noch, dass es doch ziemlich interessant aussieht. Und nach den Beispielprogrammen zu urteilen kann man auch mit der Beta schon Einiges anfangen. Wenn Sie so ähnlich wie ich gestrickt sind und über das unvermeidliche Hello World hinaus gekommen sind, hat Sie aber vermutlich ebenfalls der Schlag getroffen. Dieses .NET-Ding ist nicht einfach nur eine weitere Sprache und ganz sicher nicht einfach nur eine weitere Klassenbibliothek - es ist eine völlig neue Entwicklungsumgebung! Gelegentlich hat man einfach den Eindruck, das .NET sei ein wenig zu umfangreich, um es verstehen und beherrschen zu können.

Als ich vom MS-DOS auf Windows 3.0 umstieg und zeitweilig den Überblick darüber verlor, was eigentlich vor sich geht (jawohl, ich bin schon etwas länger dabei), sah ich mir den Code auf der Assembler-Ebene an, um mir meinen Reim darauf machen zu können. Das Schöne an der Assembler-Sprache (auch als "unzweideutiger Modus" bekannt) ist, dass sie niemals lügt. Auch beim Übergang vom Win32 auf das .NET scheint meine Welt wieder etwas auf dem Kopf zu stehen. Ohne meine Assembler-Krücke hätte ich einfach Mühe, vom Fleck zu kommen. Ich könnte mir zwar in den diversen Debuggern die Intel-Form des Assemblercodes ansehen, aber das ist diesmal keine große Hilfe, weil der direkte Bezug zum Quelltext fehlt. Anders gesagt - ich steckte fest.

Das .NET ist ein riesiger Haufen Zeugs, groß wie ein Elefant. Und wie verspeist man einen Elefanten? Häppchen für Häppchen natürlich. Ich möchte auf der Abstraktionsebene beginnen, auf der ich mir die einzelnen Operationen ansehen und zu komplexeren Abläufen zusammensetzen kann. Bei der Suche nach dieser Abstraktionsebene bin ich auf meinen neuen Freund gestoßen, den Intermediate Language Disassembler, kurz ILDASM. Der ILDASM ermöglicht die Anzeige der Pseudo-Assemblersprache vom .NET und stellt derzeit die einzige Möglichkeit dar, im .NET die Frage nach dem Wer, Was, Wann, Wo und Warum zu klären. Ich bin mir ziemlich sicher, dass ich niemals eine größere Anwendung in der MSIL schreiben werde (Microsoft intermediate language). Trotzdem ist es von Nutzen, sich in dieser Form der Assemblersprache auszukennen. Zudem ist die Dokumentation vom Visual Studio.NET für eine Beta 1 zwar ausgezeichnet, aber sie enthält trotzdem noch eine ganze Reihe von Löchern, die mit einem "[To be supplied]" notdürftig gestopft wurden. Beim Versuch, die Funktionsweise der .NET-Klassen herauszufinden, musste ich gelegentlich in den disassemblierten Code schauen.

ILDASM-Grundlagen

Die Dokumentation vom Beta 1 SDK geht kaum auf den ILDASM ein. Daher möchte ich einige Punkte beschreiben, die Ihnen die Arbeit mit dem ILDASM erleichtern werden. Der erste interessante Punkt ist, dass der ILDASM assemblierbaren Code erzeugt. Schicken Sie das Ergebnis, das der ILDASM produziert, durch den MSIL-Assembler ILASM und er wird eine brauchbare binäre Datei generieren. Die meisten Entwickler werden wohl nie direkt in MSIL programmieren. Aber der eine oder andere wird seine Spezialsprachen auf die CLR umstellen (common language runtime). Am einfachsten dürfte es in solchen Fällen sein, .IL-Dateien zu generieren und sie durch den ILASM zu schicken. Da es derzeit kaum Beispiele für die Programmierung in MSIL gibt, stellen die generierten MSIL-Dateien praktisch alles an Dokumentation dar, was derzeit verfügbar ist.

Starten Sie den ILDASM zuerst mit dem Schalter /?, damit er Ihnen seine Optionen erklärt. Mit /OUT=<Dateiname> können Sie den disassemblierten MSIL-Code in der angegebenen Datei ablegen. Zu den Kommandozeilenschaltern, die nicht aufgeführt werden, gehört /ADV. Mit /ADV erhalten Sie zusätzliche Informationen über die Datei. Diese Informationen betreffen hauptsächlich Metadaten und andere Daten über die Datei. Wenn man diese Angaben braucht, lassen sie sich nicht anderweitig ermitteln. Wenn Sie /ADV für den GUI-Modus angeben, bewirkt dieser Schalter das Erscheinen von drei neuen Menüpunkten im View-Menü.

  • "COR header" ermöglicht die Anzeige der Daten aus dem Dateikopf.

  • Statistics erlaubt die Anzeige diverser statistischer Daten über die Datei.

  • Metainfo öffnet ein weiteres Menü, in dem man die gewünschten Angaben eingrenzen und dann mit dem Befehl "Show!" oder mit der Tastenkombination Ctrl+M anzeigen lassen kann. Wird keiner der angebotenen Punkte ausgewählt, zeigt der ILDASM alle Metadateninformationen an.

Nach dem Start zeigt der ILDASM eine GUI wie in Bild B1 an. Im gezeigten Baum gibt es alle möglichen Symbole und verschiedene Typen. Wenn Sie den Baum in eine Datei schreiben, wird jeder Knoten zudem mit einem Akronym versehen, das sich aus drei Buchstaben zusammensetzt. Da die Symbole und Beschreibungen anfangs etwas verwirrend sind, habe ich sie in einer Tabelle zusammengefasst (Tabelle T1)

11

B1 Der ILDASM zeig den Dateiinhalt hierarchisch an.

T1 Die Symbole und Akronyme des ILDASM

Symbol (Glyph)

Textausgabe

Beschreibung

 

[MOD] für den Modulkopf

Direktiven, Klassendeklarationen und Manifestangaben.

 

[NSP]

Namensraum

 

[CLS]

Klasse

 

[INT]

Schnittstelle

 

[ENU]

Aufzählung

 

[VCL]

Wertklasse (Struktur)

 

[MET]

Methode (private, public oder protected)

 

[STM]

Statische Methode

 

[FLD]

Feld (private, public oder protected), auch Baugruppe (Assembly)

 

[STF]

Statisches Feld

 

[EVT]

Ereignis (Event)

 

[PTY]

Property (get und/oder set)

In der GUI vom ILDASM lassen sich sehr leicht zusätzliche Informationen über einen bestimmten Eintrag ermitteln: Man klickt den Eintrag einfach mit einem Doppelklick an. Elternknoten klappen dann auf und Kindknoten öffnen ein neues Fenster, in dem der disassemblierte Code, die Deklaration oder sonstige Informationen zu sehen sind, je nach dem angeklickten Knoten. Wenn Sie eine Anzeige wie in Bild B2 erhalten, ist es soweit: Sie sind bereit, die Assemblersprache MSIL zu lernen! Der letzte Tipp, den ich über den ILDASM noch loswerden möchte, betrifft das Ziehen-und-Ablegen. Da der ILDASM den Drag-and-Drop-Mechanismus beherrscht, können Sie sich leicht von Datei zu Datei bewegen und somit auch leicht überprüfen, welches Modul welche Klassen und Methoden einer Baugruppe enthält.

111

B2 So zeigt der ILDASM den MSIL-Code an.

CLR-Grundlagen

Bevor Sie sich nun durch die MSIL-Befehle wühlen, möchte ich etwas über die Arbeitsweise der CLR erzählen, weil diese CLR sozusagen die CPU für die MSIL-Befehle ist. Während eine herkömmliche CPU für ihre Arbeit auf Register und Stapel zurückgreifen kann, benutzt die CLR nur den Stapel. Das bedeutet, dass zur Addition von zwei Zahlen zum Beispiel beide Zahlen auf dem Stapel abgelegt werden müssen, wo sie dann mit dem entsprechenden Befehl addiert werden. Dieser Befehl entfernt die beiden Zahlen vom Stapel und legt das Ergebnis der Operation wieder auf dem Stapel ab. Falls Sie sich das nicht so recht vorstellen können, hilft vermutlich eine handfeste Implementierung weiter. Ein System, das eine gewisse Ähnlichkeit mit der CLR hat, aber wesentlich kleiner und damit leichter verständlich ist, finden Sie zum Beispiel in dem Buch "The Unix Programming Environment" von Brian Kernighan und Rob Pike (Prentice Hall, 1984). In diesem Buch implementieren die beiden Autoren einen "Rechner höherer Ordnung" (hoc, higher order calculator). Das ist ein nichttriviales C-Beispiel für eine Stapelmaschine.

Der Bewertungsstapel der CLR kann alle Wertearten aufnehmen. Die Übertragung der Werte vom Speicher auf den Stapel wird Laden (loading) genannt, die Übertragung der Werte vom Stapel in den Speicher ist die Speicherung (storing). Während es bei Intel-CPUs üblich ist, lokale Variablen auf dem Stapel unterzubringen, liegen sie in der CLR im Speicher. Die Stapel gelten jeweils lokal in der Methode, in der die Arbeit erledigt wird. Und die CLR bewahrt die Stapel von einem Methodenaufruf zum nächsten auf. Außerdem werden die Ergebnisse mit Hilfe des Stapels zum Aufrufer zurückgegeben. Nun habe ich aber genug über die Arbeitsweise der CLR erzählt und es wird Zeit für einen Blick auf die Befehle.

MSIL, Parameter und lokale Variablen

Da ich hier quasi der durchschnittliche Entwickler bin, habe ich zuerst das unvermeidliche "Hello World!" geschrieben. Damit ich sehe, was läuft. Listing L1 zeigt mein kürzestes MSIL-Programm, das den gewünschten Text anzeigt. Selbst wenn Sie nun zum ersten Mal in Ihrem Leben MSIL-Code sehen, werden Sie vermutlich schnell erkennen, wie das Programm arbeitet (Listing L2 zeigt ein längeres Beispiel). Alles, was mit einem Punkt beginnt, stellt eine Direktive für den Assembler ILASM.EXE dar. Kommentare beginnen mit dem in C++ üblichen doppelten Schrägstrich.

L1 Das unvermeidliche "Hello World!" in MSIL

/*/////////////////////////////////////////////////////// 
    System Journal - Bugslayer-Kolumne - John Robbins  
///////////////////////////////////////////////////////*/ 
// Damit das Programm läuft, braucht man eine Baugruppe. 
.assembly hello {} 
// Definiere ein an C angelehntes main. 
.method static public void main() il managed 
{ 
    // Dies ist der Hinweis für die Laufzeitschicht, wo 
    // die Programmausführung beginnen soll. Diese Direk- 
    // tive lässt sich auch auf Methoden anwenden. 
    .entrypoint 
    // Die folgende Angabe ist zwar nicht unbedingt  
    // erforderlich, aber da der ILDASM sie immer 
    // anzeigt, schreibe ich's einfach hin. 
    .maxstack 1 
    // Schiebe einen String auf den Stapel. 
    ldstr  "Hello World from IL!" 
    // Rufe die Methode System.Console.Writeline auf. 
    call   void [mscorlib]System.Console::WriteLine( 
                          class System.String) 
    // Kehre zum Aufrufer zurück. Die Datei lässt sich 
    // zwar kompilieren, wenn man ret vergisst, aber dann 
    // meldet die Laufzeitschicht eine Ausnahme. 
    ret 
}

Die wichtigen Codeteile aus Listing L1 sind die letzten drei Zeilen. Der LDSTR-Befehl sorgt dafür, dass der Text auf den Stapel gelangt. Die Übertragung auf den Stapel zählt als Ladevorgang. Also holen alle Befehle, die mit LD beginnen, irgendwelche Dinge aus dem Speicher und legen sie auf dem Stapel ab. Der umgekehrte Vorgang, den ich im "Hello World!"-Beispiel nicht brauche, ist die Speicherung (storing). Die entsprechenden Befehle beginnen alle mit ST. Mit diesen beiden kleinen Fakten bewaffnet und mit der Hilfe, die der ILDASM im disassemblierten Code durch zusätzliche Strings gibt, ist die Untersuchung des Codes nicht mehr allzu schwer.

Nach diesem ersten Blick auf die MSIL-Assemblersprache wird es Zeit für einen genaueren Blick auf die ILDASM-Anzeige, damit Sie sehen, wie die Dinge zusammenpassen.

Die Ermittlung der Parameter und Rückgabetypen ist im ILDASM kinderleicht, denn der ILDASM zeigt diese Dinge nach einem Doppelklick auf eine Methode an. Zudem erscheinen die Parameter sogar mit ihren richtigen Namen. Klassenwerte werden im Format [Modul]Namensraum.Klasse angezeigt. Die Kerntypen des Systems wie int, char und so weiter werden in Form ihrer Klassentypen angezeigt. So erscheint ein int zum Bespiel als int32.
Auch die Anzeige der lokalen Variablen ist leicht zu entziffern. Wenn die Debug-Symbole verfügbar sind, werden auch die lokalen Variablen mit ihren richtigen Namen angezeigt. Systemklassen erscheinen bei der Disassemblierung dagegen in der folgenden Form:

.locals (class [mscorlib]Microsoft.Win32.RegistryKey V_0, 
         class System.Object V_1, 
         int32 V_2, 
         int32 V_3)

Die Anweisung .locals und die Klammern kennzeichnen die vollständige Parameterliste. Die einzelnen Parameter werden jeweils durch ein Komma voneinander getrennt. Der Typ wird in einem V_#-Format angegeben, wobei die # jeweils die Parameternummer darstellt. Wie Sie später noch sehen werden, wird diese Nummer in einer ganzen Reihe von Befehlen benutzt. Das [mscorlib] im obigen Codeauszug nennt die DLL, in der die Klasse zu finden ist.

L2 Ein etwas umfangreicherer MSIL-Beispielcode

/*/////////////////////////////////////////////////////// 
    System Journal - Bugslayer-Kolumne - John Robbins  
///////////////////////////////////////////////////////*/ 
// Die Baugruppendeklaration 
.assembly SomeMath.exe { } 
// Die Referenzen 
.file System.dll 
// Ich möchte nicht auf Reflection zurückgreifen, nur 
// um den Wert von Math::PI zu ermitteln. Also definiere 
// ich den Wert selbst. 
.data thePI = float64(3.1415926535897931) 
.field public static float64 PI at thePI 
// Die Klasse Formulas 
.class public auto autochar Formulas extends [mscorlib]System.Object 
{ 
    ///////////////////////////////////////////////////// 
    // Der Klassenkonstruktor 
    ///////////////////////////////////////////////////// 
    .method public rtspecialname specialname hidebysig instance 
    void .ctor() il managed 
    { 
        // Schieb den this-Zeiger auf den Stapel 
        ldarg.0 
        // Rufe den Super-Konstruktor auf 
        call instance void [mscorlib]System.Object::.ctor() 
        ret 
    } 
    ///////////////////////////////////////////////////// 
    // Berechne die Fläche eines Rechtecks 
    //  Area = Base * Height 
    ///////////////////////////////////////////////////// 
    .method public hidebysig instance 
    int32 SquareArea ( int32 Base , int32 Height ) 
    { 
        // Prüfe das Base-Argument 
        ldarg Base 
        ldc.i4.0 
        bgt CheckHeight 
        ldstr "Formulas::SquareArea - Base <= 0" 
        ldstr "Base" 
        newobj instance void [mscorlib] 
            System.ArgumentException::.ctor ( class System.String , 
                                              class System.String  ) 
        throw 
        // Prüfe das Height-Argument 
    CheckHeight: 
        ldarg Height 
        ldc.i4.0 
        bgt DoWork 
        ldstr "Formulas::SquareArea - Height <= 0" 
        ldstr "Height" 
        newobj instance void [mscorlib] 
            System.ArgumentException::.ctor ( class System.String , 
                                              class System.String  ) 
        throw 
        // Schiebe die beiden Argumente auf den Stapel. 
        // MUL hinterlässt das Ergebnis auf dem Stapel, 
        // so dass wir anschließend gleich zum Aufrufer 
        // zurückkehren können. 
    DoWork: 
        ldarg Base 
        ldarg Height 
        mul.ovf 
        ret 
    } 
    ///////////////////////////////////////////////////// 
    // Berechne die Fläche eines Dreiecks 
    //  Area = 1/2 * Base * Height 
    ///////////////////////////////////////////////////// 
    .method public hidebysig instance 
    float64 TriangleArea ( int32 Base , int32 Height ) 
    { 
        .locals ( int32   RetVal    , 
                  float64 FloatVal   ) 
        // Prüfe das Base-Argument 
        ldarg Base 
        ldc.i4.0 
        bgt CheckHeight 
        ldstr "Formulas::TriangleArea - Base <= 0" 
        ldstr "Base" 
        newobj instance void [mscorlib] 
            System.ArgumentException::.ctor ( class System.String , 
                                              class System.String  ) 
        throw 
        // Prüfe das Height-Argument 
    CheckHeight: 
        ldarg Height 
        ldc.i4.0 
        bgt DoWork 
        ldstr "Formulas::TriangleArea - Height <= 0" 
        ldstr "Height" 
        newobj instance void [mscorlib] 
            System.ArgumentException::.ctor ( class System.String , 
                                              class System.String  ) 
        throw 
    DoWork: 
        // Schiebe den this-Zeiger auf den Stapel 
        ldarg.0 
        // Schiebe Grundlinie und Höhe auf den Stapel 
        ldarg Base 
        ldarg Height 
        // Rufe die Methode SquareArea auf 
        call instance int32 Formulas::SquareArea ( int32 , int32 ) 
        // Lege das Ergebnis in RetVal ab 
        stloc RetVal 
        // Schiebe die Adresse des Ergebnisses auf den  
        // Stapel 
        ldloca.s RetVal 
        // Konvertiere den int32-Wert in ein float64 und 
        // hinterlasse das Ergebnis auf dem Stapel 
        call instance float64 [mscorlib]System.Int32::ToDouble ( ) 
        // Schiebe die 2 als Gleitkommazahl auf den 
        // Stapel 
        ldc.r8 2.0 
        // Führe die Division durch 
        div 
        // Der Rückgabewert auf dem Stapel ist das  
        // Ergebnis der Division 
        ret 
    } 
    ///////////////////////////////////////////////////// 
    // Berechne mit dem Integer-Radius die Fläche eines 
    // Kreises 
    //  Area = pi * r^2 
    ///////////////////////////////////////////////////// 
    .method public hidebysig instance 
    float64 CircleArea ( int32 Radius ) 
    { 
        .locals ( float64 FloatVal ) 
        // Prüfe das Radius-Argument 
        ldarg Radius 
        ldc.i4.0 
        bgt DoWork 
        ldstr "Formulas::CircleArea(int32) - Radius <= 0" 
        ldstr "Radius" 
        newobj instance void [mscorlib] 
            System.ArgumentException::.ctor ( class System.String , 
                                              class System.String  ) 
        throw 
    DoWork: 
        // Wandle den Radius in ein float um und rufe 
        // dann die Funktion zur Flächenberechnung auf. 
        // Schiebe die Adresse des Arguments auf den  
        // Stapel und konvertiere das Argument in  
        // ein double. 
        ldarga.s Radius 
        // Konvertiere den int32 in ein float64 und lass 
        // das Ergebnis auf dem Stapel liegen. 
        call instance float64 [mscorlib]System.Int32::ToDouble ( ) 
        // Nimm das konvertierte Argument vom Stapel 
        stloc FloatVal 
        // Rufe die Funktion zur Flächenberechnung auf 
        // Schiebe den this-Zeiger auf den Stapel 
        ldarg.0 
        ldloc FloatVal 
        call instance float64 Formulas::CircleArea ( float64 ) 
        // Der Rückgabewert liegt auf dem Stapel 
        ret 
    } 
    ///////////////////////////////////////////////////// 
    // Berechne die Fläche eines Kreises mit einem 
    // double-Radius 
    //  Area = pi * r^2 
    ///////////////////////////////////////////////////// 
    .method public hidebysig instance 
    float64 CircleArea ( float64 Radius ) 
    { 
        .locals ( float64 FloatVal ) 
        // Prüfe das Radius-Argument 
        ldarg Radius 
        ldc.r8 0.0 
        bgt DoWork 
        ldstr "Formulas::CircleArea(float64) - Radius <= 0" 
        ldstr "Radius" 
        newobj instance void [mscorlib] 
            System.ArgumentException::.ctor ( class System.String , 
                                              class System.String  ) 
        throw 
    DoWork: 
        // Multipliziere den Radius mit sich selbst 
        ldarg Radius 
        ldarg Radius 
        mul 
        // Schiebe PI auf den Stapel 
        ldsfld float64 PI 
        // Multipliziere das Radius-Quadrat mit PI 
        mul 
        // Das Ergebnis liegt als Rückgabewert auf 
        // dem Stapel 
        ret 
    } 
}

Die wichtigen Befehle

Statt Sie nun mit einer großen Funktionstabelle zu langweilen möchte ich mich lieber auf die wichtigsten Befehle konzentrieren, auf die Sie häufiger stoßen werden, und sie an einigen Beispielen erläutern. Beginnen möchte ich mit den Ladebefehlen, die ich relativ ausführlich beschreibe. Wenn ich dann zu anderen Befehlstypen übergehe, kann ich die Teile überspringen, die praktisch schon bei den Ladebefehlen beschrieben wurden, und mich auf die Unterschiede oder die Anwendung beschränken. Die meisten Befehle, die ich hier nicht näher erläutere, haben sozusagen selbstbeschreibende Namen. So führen add und sub zum Beispiel die Addition und Subtraktion durch.

LDC (load numeric constant). Dieser Befehl schiebt eine fest einkodierte Zahl auf den Stapel. Das Befehlsformat ist LDC.Größe[.Zahl], wobei mit Größe die Größe des Werts in Byte gemeint ist und die Zahl eine spezielle Kurzform für ein 4-Byte-Integer im Bereich von -128 bis 127 ist (die Größe ist dabei I4). Die Größenangabe lautet entweder auf I4 (4-Byte-Integer), I8 (8-Byte-Integer), R4 (4-Byte-Fließkomma) oder R8 (Fließkomma). Damit die Zahl der Befehlscodes nicht ins Uferlose wächst, gibt es diesen Befehl in einer ganzen Reihe von Formen.

ldc.i4.0                   // Lade 0 auf den Stapel, in der  
                           // speziellen Form. 
ldc.r8  2.1000000000000001 // Lade 2.1000000000000001. 
ldc.i4.m1                  // Lade -1 auf den Stapel. Das ist 
                           // die spezielle Form. 
ldc.i4.s -9                // Lade -9 auf den Stapel, in der 
                           // Kurzform.

LDARG und LDARGA (load argument und load argument address). Die Nummern der Argumente beginnen mit 0. In Instanzmethoden ist das Argument 0 zum Bespiel der this-Zeiger und das erste explizit vom aufrufenden Programm übergebene Argument hat die Nummer 1 statt der 0.

ldarg.2               // Schiebe Argument 2 auf den Stapel. Die 
                      // höchste Zahl in dieser Befehlsform ist 3. 
ldarg.s 6             // Lade Argument 6 auf den Stapel. Für alle 
                      // Argumente ab Nummer 4 (einschließlich) 
                      // wird diese Form eingesetzt. 
Ldarga.s newSample    // Lade die Adresse von newSample.

LDLOC und LDLOCA (load local variable und load local variable address). Lädt die angegebene lokale Variable auf den Stapel. Die lokalen Variablen werden in der Reihenfolge durchnummeriert, in der sie deklariert werden. Der Befehl ldloca lädt die Adresse der lokalen Variablen auf den Stapel.

ldloc.0            // Lade die lokale Variable 0 auf den Stapel. Die 
                   // höchste Nummer in dieser Befehlsform ist 3. 
ldloc.s V_6        // Lade die lokale Variable 6 auf den Stapel.  
                   // Für alle lokalen Variablen ab der Nummer 4 
                   // (einschließlich) wird diese Form benutzt. 
ldloca.s V_5       // Lade die Adresse der lokalen Variablen 5 auf  
                   // den Stapel.

LDFLD und LDSFLD (Load Object Field und Load Static Field of a Class). Diese Befehle laden ein normales oder statisches Feld von einem Objekt auf den Stapel. Die Bestandteile eines Objekts sind in der MSIL-Form leicht zuzuordnen, weil die vollständige Feldbezeichnung angegeben wird. Der Befehl ldflda lädt die Adresse des angegebenen Felds.

// Lade das Feld _Originator von System.Reflection.AssemblyName.  
// Hierbei wird auch der Feldtyp angegeben. 
ldfld      unsigned int8[] System.Reflection.AssemblyName::_Originator 
// Lade den leeren String von System.String. 
ldsfld     class System.String [mscorlib]System.String::Empty

LDELEM (load an element of an array). Dieser Befehl lädt das angegebene Element von einem eindimensionalen Array auf den Stapel, wobei die Zählung der Elemente mit 0 beginnt. Die beiden Ladebefehle im folgenden Beispielcode legen das Array und den Index auf dem Stapel ab (in dieser Reihenfolge). Ldelem entfernt das Array und den Index vom Stapel und legt das angegebene Element auf der Stapelspitze ab. An den ldelem-Befehl schließt sich ein Typfeld an. In der kompilierten Basisklassenbibliothek ist ldelem.ref das gebräuchlichste Typfeld. Es sorgt für die Beschaffung des angegebenen Elements als Objekt. Andere gebräuchliche Typen sind ldelem.i4 für die Beschaffung des Elements als eine 4-Byte-Integer mit Vorzeichen und ldelem.i8 zur Beschaffung einer 64-Bit-Integer.

.locals ( System.String[] V_0,    // Ein Array wird mit [] deklariert. 
          int32 V_1 )             // Der Index. 
...                               // (V_0 erhält seinen Inhalt) 
ldloc.0                           // Lade das Array. 
ldc.i4.0                          // Lade den Index 0. 
ldelem.ref                        // Beschaffe das Objekt am Index 0.

LDLEN (load the length of an array). Dieser Befehl entfernt ein eindimensionales Array, dessen Elemente von null an gezählt werden, vom Stapel und hinterlässt die Länge des Arrays auf dem Stapel.

// Lade das Attributfeld - es ist ein Array. 
ldfld class System.ComponentModel.MemberAttribute[] 
   System.ComponentModel.MemberDescriptor::attributes 
stloc.1                    // Speichere den Wert in der ersten lokalen 
                           // Variablen (ein Array). 
ldloc.1                    // Lade die erste lokale Variable auf  
                           // den Stapel. 
ldlen                      // Ermittle die Länge des Arrays.

STARG (store a value in an argument slot). Nimmt den obersten Wert vom Stapel und speichert ihn im angegebenen Argument ab.

starg.s  categoryHelp            // Speichere den obersten Wert vom 
                                 // Stapel in categoryHelp ab. Alle 
                                 // starg-Befehle halten sich an die 
                                 // .s-Form.

STELEM (store an element of an array). Während die ersten drei Befehle im folgenden Beispiel das eindimensionale, von null an gezählte Array mit dem gewünschten Index und dem zu speichernden Wert auf dem Stapel ablegen (in dieser Reihenfolge), konvertiert der Befehl stelem den Wert in den passenden Typ, bevor er ihn im Array speichert. Der stelem-Befehl entfernt alle drei Objekte vom Stapel. Wie beim ldelem-Befehl bestimmt ein Typfeld die Konvertierung. Die gebräuchlichste Konvertierung ist die Umwandlung in ein Objekt mit stelem.ref.

.method public hidebysig specialname 
instance void  set_MachineName(class System.String 'value') il managed 
{ 
  .maxstack  4 
  .locals (class System.String[] V_0) 
... 
   ldloc.0                     // Lade das Array auf den Stapel. 
   ldc.i4.1                    // Lade den Index, die Konstante 1. 
   ldarg.1                     // Lade das Argument, den String. 
   stelem.ref                  // Speichere das Element ab. 
...

STFLD (store into a field of an object). Nimmt den obersten Wert vom Stapel und bringt ihn im angegebenen Objektfeld unter. Wie beim Laden eines Feldes wird wieder der vollständige Name angegeben.
stfld int32[] System.Diagnostics.CategoryEntry::HelpIndexes
CEQ (compare equal). Dieser Befehl vergleicht die beiden obersten Werte, die auf dem Stapel liegen. Beide Werte werden vom Stapel entfernt. Sind beide Werte gleich, hinterlässt der Befehl eine 1 auf dem Stapel, andernfalls eine 0.

ldloc.1                    // Lade die lokale Variable 1. 
ldc.i4.0                   // Lade die Konstante 0. 
ceq                        // Sind beide Werte gleich?

CGT (compare greater than). Auch dieser Befehl vergleicht die beiden obersten Werte auf dem Stapel. Beide Werte werden vom Stapel entfernt. Ist der Wert, der zuerst auf den Stapel geschoben wurde, größer als der zweite, so wird eine 1 auf dem Stapel abgelegt, andernfalls eine 0. Der Befehl cgt lässt sich auch mit dem Modifizierer .un anwenden, der besagt, dass der Vergleich ohne Vorzeichen oder vorgegebene Ordnung erfolgt.

// Ermittle die Anzahl der Elemente in der Sammlung. 
call instance int32 System.Diagnostics. 
  CounterCreationDataCollection::get_Count() 
ldc.i4.0                    // Lade die Konstante 0. 
cgt                         // Ist die Zahl der Elemente 
                            // größer als null?

CLT (compare less than). Dieser Befehl ähnelt cgt, mit dem Unterschied, dass eine 1 auf den Stapel geschoben wird, wenn der erste Wert kleiner als der zweite ist.

// Ermittle den TraceLevel. 
call instance value class System.Diagnostics.TraceLevel    
         System.Diagnostics.TraceSwitch::get_Level() 
ldc.i4.1                    // Lade die Konstante 1. 
clt                         // Ist der TraceLevel kleiner als 1?

BR (unconditional branch). Dieser Befehl ist das goto von MSIL.

br.s IL_008d                // Gehe in der Methode zur angegebenen 
                            // Position.

BRFALSE und BRTRUE (branch on false und branch on true). Beide Befehle untersuchen den obersten Wert auf dem Stapel. Vom Ergebnis dieser Untersuchung hängt es ab, ob sie zur angegebenen Adresse springen oder nicht. Der Befehl brtrue springt nur dann zur neuen Adresse, wenn er eine 1 vorfindet, während brfalse nur bei einer 0 die neue Adresse anspringt. Beide Befehle entfernen den untersuchten Wert vom Stapel.

ldloc.1                       // Lade die lokale Variable 1. 
brfalse.s  IL_006a            // Springe neue Adresse an, falls 0. 
ldloc.2                       // Lade die lokale Variable 2. 
brtrue.s   IL_006c            // Springe neue Adresse an, falls 1.

Tabelle T2 listet die restlichen Sprungbefehle auf. In allen Fällen werden die beiden obersten Werte auf dem Stapel untersucht und der oberste Wert mit dem nächsten verglichen. In allen Fällen hat der bedingte Sprung die Form eines Vergleichs mit einer entsprechenden Auswertung des Boolean-Ergebnisses. So entspricht BGT zum Beispiel dem cgt-Befehl, gefolgt von einem brtrue.
T2 Bedingte Sprünge

Befehl

Beschreibung

BEQ

Springe bei Gleichheit

BGT

Springe wenn größer

BLE

Springe wenn kleiner oder gleich

BLT

Springe wenn kleiner

BNE

Springe wenn ungleich

CONV (data conversion). Dieser Befehl konvertiert das oberste Element vom Stapel in einen neuen Typ und hinterlässt den umgewandelten Typ wieder auf der Stapelspitze. Hinter dem conv-Befehl wird der gewünschte Zieltyp angegeben. So konvertiert conv.u4 den Wert zum Beispiel in ein 4-Byte-Integer ohne Vorzeichen. Der conv-Befehl mit dem Zieltyp führt nicht zur Meldung einer Ausnahme, falls es zu irgendeinem Überlauf kommt. Steht zwischen dem conv und dem Zieltyp aber noch ein zusätzliches .ovf wie in conv.ovf.u8, so wird im Falle eines Überlaufs eine entsprechende Ausnahme gemeldet.

ldloc.0                      // Lade die lokale Variable 0 
                             // (ein Array). 
Ldlen                        // Ermittle die Arraylänge. 
conv.i4                      // Konvertiere die Längenangabe in 
                             // einen 4-Byte-Wert.

NEWARR (create a zero-based, one-dimensional array). Dieser Befehl legt ein neues Array vom gewünschten Typ mit so vielen Elementen an, wie das oberste Element auf dem Stapel vorgibt. Die Angabe der Elementezahl wird vom Stapel entfernt und das fertige Array wird auf dem Stapel abgelegt.

ldc.i4.5                    // Das Array soll 5 Elemente erhalten. 
// Lege das neue Array an. 
newarr System.ComponentModel.MemberAttribute

NEWOBJ (create a new object). Legt ein neues Objekt an und ruft den Konstruktor des Objekts auf. Alle Konstruktorargumente werden über den Stapel übergeben. Sofern der Objektbau erfolgreich ist, werden die Argumente vom Stapel entfernt und Objektreferenz auf dem Stapel hinterlassen.

.method public hidebysig specialname rtspecialname 
        instance void  .ctor(class [mscorlib]System.IO.Stream 'stream', 
                             class System.String name) il managed 
{ 
... 
   ldarg.1                    // Lade stream-Argument. 
                              // Lege das neue Objekt an. 
   newobj instance void [mscorlib] 
          System.IO.StreamWriter::.ctor(class  
                                        [mscorlib]System.IO.Stream)

BOX (convert value type to object reference). Dieser Befehl verpackt einen Wert in ein Objekt und hinterlässt dieses Objekt auf dem Stapel. Sobald die zeitweilige Umwandlung eines Werttyps in einen Referenztyp erforderlich ist, erledigt dieser Befehl die Arbeit. Sie werden ihn in Codesequenzen wie in Listing L3 häufiger antreffen, wenn Argumente übergeben werden.

L3 Boxing - ein Werttyp erhält eine "Umverpackung"

// An diese Methode wird ein INT32-Werttyp übergeben 
.method public hidebysig specialname 
        instance void  set_Indent(int32 'value') il managed 
{ 
... 
ldstr     "Indent"  // Schiebe den Methodennamen auf  
                    // den Stapel. 
ldarga.s  'value'   // Lade die Adresse des Parameters. 
          // Konvertiere den Werttyp in einen  
          // Referenztyp. 
box       [mscorlib]System.Int32   
          // Lade die Nachricht. 
ldstr     "The Indent property must be non-negative." 
          // Lege eine neue ArgumentOutOfRangeException 
          // an. 
newobj    instance void [mscorlib]System.ArgumentOutOfRangeException:: 
            .ctor(class System.String, 
                  class System.Object, 
                  class System.String)

UNBOX (convert boxed value type to its raw form). Dieser Befehl liefert eine verwaltete Referenz auf den Werttyp aus der verpackten Form. Die Referenz ist keine Kopie, sondern der tatsächliche Objektzustand. Im kompilierten C#- und Visual Basic.NET-Code folgt auf einen unbox-Befehl ein ldind (load value indirect onto the stack) oder ldobj (copy value type to the stack).

// Konvertiere den Wert in ein System.Reflection.Emit.LocalToken 
unbox System.Reflection.Emit.LocalToken 
// Lade den Wert auf den Stapel 
ldobj System.Reflection.Emit.LocalToken 
unbox [mscorlib]System.Int16   // Konvertiere den Wert in ein 
                            // Int16-Objekt. 
ldind.i2                    // Lege den Objektwert auf dem Stapel ab.

CALL und CALLVIRT (call a method und call a method associated at runtime with an object). Der call-Befehl ruft statische und nichtvirtuelle normale Methoden auf. Virtuelle Methoden und Schnittstellenmethoden werden mit dem Befehl callvirt aufgerufen. Die Argumente werden von links nach rechts auf den Stapel gelegt. Beachten Sie bitte, dass diese Reihenfolge von den meisten Aufrufkonventionen abweicht, die in der IA32-Welt üblich sind. Listing L4 zeigt ein Beispiel für den Einsatz von callvirt.

L4 So erfolgt ein Methodenaufruf mit callvirt

// Lade den Parameter. 
ldfld class System.String  
 System.CodeDOM.Compiler.CompilerResults::pathToAssembly 
// Rufe die virtuelle Methode set_CodeBase auf. 
callvirt   instance void [mscorlib] 
  System.Reflection.AssemblyName::set_CodeBase  
                                     (class System.String) 
... 
ldarg.0        // Lade den this-Zeiger. Er ist immer der 
               // erste Parameter. 
ldarg.1        // Lade Argument 1. 
ldnull         // Lade einen Nullwert. 
               // Rufe die virtuelle Funktion auf. 
callvirt instance void  
  System.Diagnostics.TraceListener::Fail(class System.String 
                                         class System.String) 
ret            // Kehre zum Aufrufer zurück.

Fazit

Der in Listing L2 gezeigte Code ist ein Auszug aus einem MSIL-Programm, das ich als kleine Übung geschrieben habe, um MSIL zu lernen. Das vollständige Programm finden Sie auf der Begleit-CD dieses Hefts. Bei den gelegentlich etwas schwerfälligen Wanderungen durch die Beta-Landschaft kann es hilfreich sein, sich den generierten MSIL-Code im ILDASM anzuschauen. Außerdem lässt sich auch das Gesamtbild leichter verstehen, wenn man weiß, wie die Dinge auf der untersten Ebene funktionieren. Falls Sie mehr über MSIL erfahren möchten, sollten Sie auf jeden Fall die zusätzliche Dokumentation in "...\Program Files\Microsoft.Net\FrameworkSDK\Tool Developers Guide" auspacken. Die beiden wichtigsten Dateien sind ILINSTRSET.DOC und ILAssemblyLanguageProgrammersReference.DOC.

Wie Sie sehen, ist es gar nicht so schwer, eine .NET-Anwendung zu analysieren. Damit die Installation per xcopy und die Metadaten-Mechanismen möglich werden, muss schon einiges an Information in der Binärdatei stehen. Daher lässt sich auch viel einfacher ermitteln, was im Programm geschieht. Die Java-Sprache hat dieselben Probleme und es gibt sogar Decompiler, die den Bytecode wieder in den Java-Quellcode zurückverwandeln. Allerdings hat das keinen nennenswerten Einfluss auf die Java-Entwicklung gehabt. Die leichte Disassemblierbarkeit wird die Entwicklung des .NET-Codes wohl auch nicht nennenswert bremsen.

Die meisten Leser werden sich vermutlich mit ASP.NET beschäftigen, weil es die Entwicklung für das Web so unglaublich einfach und leistungsfähig macht. Da anschließend alles auf dem Server läuft, ergibt sich für die Anwender oder außenstehende Entwickler normalerweise auch nicht die Möglichkeit, Ihre geheimsten Algorithmen zu entziffern. Client-Anwendungen lassen sich im .NET zwar im Prinzip disassemblieren, aber die ungewöhnlichen positiven Aspekte des .NETs überwiegen bei weitem die leichte Disassemblierbarkeit.

Die Debugging-Tipps

Tipp 43 Pavel Lebedinsky fand einen wunderschönen Trick für den Debugger von Visual C++ 6.0 heraus, der tief in der KnowledgeBase von Microsoft vergraben war: Der Debugger kann Crash-Dump-Dateien lesen! KnowledgeBase-Artikel Q248115 nennt den geheimen Registrierschlüssel, der für das Laden der Crash-Dumps sorgt. Setzt man den REG_DWORD-Wert CrashDumpEnabled unter "HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\Debug" auf 1, erscheint beim Öffnen der Arbeitsbereiche zusätzlich die Option *.DMP. Es hat zwar ganz den Anschein, als sei die Sache nur halb implementiert, aber es funktioniert. Am besten kopieren Sie die PDB-Dateien, die für den abgestürzten Prozess erforderlich sind, ins selbe Verzeichnis, in dem auch die .DMP-Datei liegt.

Tipp 44 Mike Morearty war der Ansicht, es sei nicht schlecht, wenn man im Debugger von Visual C++ 6.0 richtige Hardware-Haltepunkte hätte (read und write). Also hat er eine schnuckelige kleine Klasse namens CBreakpoint geschrieben, mit deren Hilfe man die zu überwachende Adresse festlegen kann. Das Programm stoppt bei jedem echten Schreib- oder Lesezugriff auf diese Adresse. Mikes Klasse geht aber weit über die Datenzugriffshaltepunkte hinaus, die derzeit im Debugger geboten werden. Es ist eine fantastische Klasse, die mir bereits dabei geholfen hat, einige schwer aufzuspürende Fehler zu finden! Sie können den vollständigen Code von http://www.morearty.com/code/breakpoint herunterladen. Außerdem beschreibt Mike in einem netten kleinen Dokument, wie man diese Klassen einsetzt.

Anzeigen:
© 2014 Microsoft