Verwalteter Code hinter den Kulissen, Teil 3

Veröffentlicht: 08. Sep 2005
Von Peter Koen

Erst auf der IL-Ebene ist genau zu sehen, wie Microsoft es geschafft hat, dass sich viele Sprachen in dieselbe Laufzeit kompilieren lassen. In diesem letzten Teil der IL-Assemblerserie erfahren Sie, wie man objektorientierten IL-Code schreibt, Felder deklariert, Methoden und Eigenschaften erstellt sowie Objektinstanzen erzeugt und auf ihre Member zugreift.

dot.net magazin


Diesen Artikel können Sie dank freundlicher Unterstützung von dot.net magazin auf MSDN Online lesen. dot.net magazin ist ein Partner von MSDN Online.


Zur Partnerübersichtsseite von MSDN Online

Im ersten Teil dieser Artikelserie haben Sie die Grundlagen der Ausführung von verwaltetem Code kennen gelernt und Ihre erste ILAsm-Anwendung erstellt - das allseits bekannte "Hello World"-Beispiel. Der zweite Teil hat sich mit grundlegenden Programmieraufgaben wie zum Beispiel Schleifen und dem Steuerungsfluss beschäftigt. Jetzt erfahren Sie, wie Sie objektorientierte Programmierung mit Intermediate Language Assembler (ILAsm) verwenden, um Felder, Instanzmethoden und Eigenschaften zu deklarieren sowie Instanzen zu erstellen und ihre Member aufzurufen. Außerdem fügen Sie Ihren Klassen weitere Metadaten mit Attributen hinzu. Auf komplexere Dinge wie Delegaten geht dieser Artikel allerdings nicht ein, da sie den vorgesehenen Rahmen sprengen würden. Anhand der angegebenen Quellen und Links am Ende dieses Artikels können Sie aber herausfinden, wie das zu bewerkstelligen ist.

Auf dieser Seite

 Ein Kugelbeispiel
 Das Manifest erweitern
 Assembly-Details spezifizieren
 Klassendeklaration und Namespace hinzufügen
 Die Klassen-Member definieren
 Felder implementieren
 Konstruktoren hinzufügen
 Methoden erzeugen
 Eigenschaften definieren
 Attribute hinzufügen
 Typen instanziieren
 Zusammenfassung
 Links & Literatur
 Über den Autor

Ein Kugelbeispiel

Der Beispielcode für diesen Artikel ist ein weiteres arithmetisches Projekt - eine Kugelklasse, die Sie konstruieren, indem Sie den Radius übergeben, und die Eigenschaften für Radius und Volumen bietet. Um das Beispiel etwas komplexer zu machen und die Verwendung von Attributen in ILAsm zu demonstrieren, erfahren Sie auch, wie Sie in ILAsm Metadaten in Klassen einfügen.

Das Manifest erweitern

Auch das Manifest fällt etwas komplizierter aus. Im ersten Artikel haben Sie nur den Assembly-Namen verwendet und im zweiten Artikel die Versionsnummer hinzugefügt. Es besteht aber immer noch die Gefahr von Verwechslung und Missbrauch. Deshalb fügen Sie jetzt den Token für einen öffentlichen Schlüssel hinzu, um der CLR mitzuteilen, dass Sie eine signierte Assembly mit einem bestimmten öffentlichen Schlüssel erwarten. Der Code sieht wie folgt aus:

.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
  .ver 1:0:5000:0
}

.assembly extern System.Xml
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
  .ver 1:0:5000:0
}

Wie üblich brauchen Sie mscorlib als Basis-Assembly, für dieses Beispiel ist aber auch System.Xml erforderlich, da das Beispiel die XmlSerializer-Klasse aus diesem Namespace verwendet. Woher bekommen Sie die Werte für die Publickeytoken- Direktiven? Ganz einfach: Die hier angegebenen Schlüsselwerte sind dieselben Schlüssel, die im globalen Assembly Cache (GAC) des Windows Explorer zu sehen sind. Um den Schlüssel für eine Assembly zu erhalten, die sich im globalen Assembly Cache befindet, gehen Sie in Windows Explorer zu \assembly\.

Assembly-Details spezifizieren

Nach den notwendigen Importen müssen Sie das Manifest vervollständigen und die Details für die Assembly, in der sich Ihre Klasse befindet, spezifizieren.

.assembly Sphere
{
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}

.module Sphere.exe
.subsystem 0x00000003

Dieser Code ähnelt stark dem Beispielcode aus dem letzten Artikel, nur die .hash-Direktive ist neu. Sie spezifiziert den Algorithmus, nach dem das Framework den kryptographischen Hash-Wert berechnet, mit dem diese Assembly signiert wird. Der hier angegebene Standardwert (0x8004) steht für den SHA1-Algorithmus, den alle CLI- kompatiblen Implementierungen verwenden müssen. Microsoft hat SHA1 als die beste der weithin verfügbaren Technologien zum Zeitpunkt der Standardisierung auserkoren. Ein einziger Algorithmus ist die praktischste Lösung, weil von allen konformen Implementierungen der CLI verlangt wird, alle Algorithmen zu implementieren, um die Portabilität der ausführbaren Bilder sicherzustellen. Beachten Sie, dass alle anderen Werte momentan für die zukünftige Verwendung reserviert sind.

Klassendeklaration und Namespace hinzufügen

Das Manifest ist damit fertig und Sie können einen Namespace und eine Vorwärtsdeklaration für die Klassen in diesem Namespace hinzufügen. Hier könnten Sie auch die Member der Klasse angeben, obwohl es zweckmäßiger ist, zuerst alle Klassen zu deklarieren und später die Implementierung ihrer Member einzufügen.

Der im Kasten enthaltene Code sieht fast wie C# aus, auch wenn einige Unterschiede auffallen: Wie immer bei ILAsm beginnen alle Direktiven mit einem Punkt. Die Namespace-Deklaration ist einfach eine Container-Deklaration wie in den meisten Hochsprachen und verwendet - wie Sprachen im C-Stil - geschweifte Klammern, um den Gültigkeitsbereich - und folglich die Member - dieses Namespace zu definieren. Die .class-Direktive ist etwas komplexer. Die angegebenen Schlüsselwörter haben folgende Bedeutung:

  • public - Dieses Schlüsselwort hat die gleiche Bedeutung wie in den meisten Hochsprachen: Die Klasse soll für alle sichtbar sein.

  • auto - Das Framework soll automatisch das Speicherlayout der Feld-Member generieren.

  • ansi - Alle Zeichenfolgen, die auf die Plattform als ANSI-Zeichenfolgen zu marshallen sind, werden gemarshallt.

  • serializable - Gibt an, dass dieser Typ serialisiert werden kann (das serializable-Attribut ist hier erforderlich, weil Sie später den XmlSerializer hinzufügen.) Beachten Sie, dass das Erstellen einer serialisierbaren Klasse in den meisten .NET- Hochsprachen das Serializable Attribute verlangt.

  • beforefieldinit - Dieses Schlüsselwort spezifiziert, dass der Aufruf einer statischen Methode den Typ nicht initialisiert. Es sagt der CLI, dass sie keinen Konstruktor des Typs vor dem Aufrufen einer statischen Methode aufzurufen braucht.

  • Sphere - Der Name der Beispielklasse. Im konkreten Fall ist dieser Name mit dem Namespace identisch - das ist aber nur eine zufällige Übereinstimmung.

  • extends - Dies ist das Schlüsselwort für Vererbung. Auf dieses Schlüsselwort folgt eine Liste von Klassen und Schnittstellen, die dieser Typ erweitert.

  • [mscorlib]System.Object - Dies ist der Basistyp, den der aktuelle Typ erweitert. Es ist auch möglich, einen anderen Typ zu erweitern, aber jeder CLI-kompatible Typ muss diesen Basistyp erweitern.

Die Klassen-Member definieren

Nachdem Sie die Vorwärtsdeklaration der Klassen in Ihrem Namespace eingefügt haben, können Sie die Klassen-Member implementieren. Beginnen Sie wie gehabt mit dem Namespace und der Klasse:

// CLASS MEMBERS DECLARATION
.namespace Sphere
{
.class public auto ansi serializable beforefieldinit Sphere extends [mscorlib]System.Object
{

Beachten Sie, dass dieser Code genau der gleiche wie in der Vorwärtsdeklaration ist.

Felder implementieren

Felder lassen sich äußerst einfach implementieren. Geben Sie eine .field-Direktive an und spezifizieren Sie dann den Zugriffsmodifizierer, den Typ und den Namen genau wie in jeder anderen Hochsprache:

.field private float64 radius

Dieser Code fügt ein privates Feld namens radius mit dem Typ float64 hinzu. ILAsm kennt die folgenden Zugriffsmodifizierer:

  • assembly

  • famandassem (family und assembly)

  • family

  • famorassem (family oder assembly)

  • private

  • public

Hinweis: Die meisten Hochsprachen verwenden das Schlüsselwort protected anstelle von family.

Konstruktoren hinzufügen

Konstruktoren sind Methoden einer Klasse wie jede andere Methode. Sie haben eine besondere Bedeutung: einen speziellen Namen und auch spezielle Direktiven.

Listing 1 spezifiziert zwei Konstruktoren. Der erste (der keinen Parameter verlangt) ruft den zweiten auf, der ein float- 64-Argument übernimmt, um den Radius der Kugel zu initialisieren. In einer Hochsprache wie C# würde man hierfür eine Initialisierungsliste einsetzen. Bevor Sie sich den internen Code des Konstruktors zu Gemüte führen, sollten Sie einen Blick auf die Deklarationsdirektiven werfen:

  • .method - Der folgende Codeblock ist eine Methode.

  • public - Der Konstruktor muss für alle aufrufbar sein.

  • hidebysig - Kurz für "hide by signature" (nach Signatur verbergen); dies wird durch die Laufzeit ignoriert.

  • specialname- Der Methodenname muss in besonderer Weise durch ein Tool behandelt werden.

  • rtspecialname - Der Methodenname muss in besonderer Weise durch die Laufzeit behandelt werden.

  • instance- Diese Methode ist eine Instanzmethode und keine statische Methode.

  • void - Der Rückgabetyp des Konstruktors (in den meisten Hochsprachen besitzen Konstruktoren keinen Rückgabetyp.)

  • .ctor - Der Name der Methode; .ctor ist für Konstruktoren reserviert.

  • (), (float64 radius) - Die Argumentliste des Konstruktors.

  • cil - Diese Methode enthält CIL-Code.

  • managed - Diese Methode enthält Code, der von der Laufzeit verwaltet werden kann (keinen unsicheren Code).

.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
  .maxstack 2
  ldarg.0
  ldc.r8 0.0
  call instance void
   Sphere.Sphere::.ctor(float64)
  ret
}


.method public hidebysig specialname rtspecialname
 instance void .ctor(float64 radius) cil managed
{
  .maxstack 2
  ldarg.0
  call instance void
    [mscorlib]System.Object::.ctor()
  ldarg.0
  ldarg.1
  stfld float64 Sphere.Sphere::radius
  ret
}

Listing 1

Sehen Sie sich nun die Implementierung des Konstruktors an:

ldarg.0
ldc.r8 0.0
call instance void Sphere.Sphere::.ctor(float64)

Da der Konstruktor eine Instanzmethode ist, muss die Laufzeit wissen, auf welcher Instanz diese Methode aufzurufen ist. Das erste Argument speichert den Verweis auf die Instanz; deshalb enthält der Code einen ldarg.0-Aufruf, um den Verweis zu laden. Dann lädt er einen Standardwert für den Radius als float64 mit dem Wert 0.0 und ruft den überladenen Konstruktor mit dem Radiuswert auf dem Stapel als den einen erforderlichen Parameter auf. Beachten Sie, dass vor der Methodensignatur das Schlüsselwort instance stehen muss, um der Laufzeit mitzuteilen, dass Sie keine statische Methode aufrufen. Da der Konstruktor einen anderen aufruft, braucht er nicht den Konstruktor der Basisklasse zuerst aufzurufen - das erledigt der zweite Konstruktor.

ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ldarg.0
ldarg.1
stfld float64 Sphere.Sphere::radius

Der Code ruft also [mscorlib] System.Object::.ctor() auf, d.h. den Konstruktor der Basisklasse System.Object, um die Instanz zu erzeugen. Als Nächstes muss das für den Radius übergebene Argument im entsprechenden Feld gespeichert werden. Der Konstruktor bewerkstelligt das mit dem Befehl stfld, der "store field" (Feld speichern) bedeutet.

Methoden erzeugen

Die Beispielklasse Sphere legt eine Calc- Volume-Methode offen, die das Volumen der Kugel berechnet. (Listing 2)

.method family hidebysig instance float64
                                                                 CalcVolume()
cil managed
{
  .maxstack 2
  ldarg.0
  ldfld float64 Sphere.Sphere::radius
  ldarg.0
  ldfld float64 Sphere.Sphere::radius
  mul
  ldarg.0
  ldfld float64 Sphere.Sphere::radius
  mul
  ldc.r8 3.1415926535897931
  mul
  ret
}

Listing 2

Die Methode CalcVolume besitzt den Zugriffsmodifizierer family, was einem Schlüsselwort protected in C# entspricht. Die Direktive instance sagt der Laufzeit, dass diese Methode nur auf einer Instanz der Klasse Sphere aufgerufen werden kann - d.h. diese Methode ist nicht statisch. Beachten Sie, dass Sie immer den Verweis auf das Objekt (den this-Zeiger oder Me in Visual Basic .NET) im Auswertungsstapel brauchen, um den Feldwert zu laden; daher steht ein ldarg.0 vor jeder ldfld- Anweisung, um den Verweis auf Sphere im Stapel abzulegen. Der Befehl ldfld bedeutet "load field" (Feld laden).

Eigenschaften definieren

In ILAsm implementieren Sie Eigenschaften immer durch zwei separate Methoden:

eine get_-Methode und eine set_-Methode:

.property instance float64 Radius()
{
  .custom instance void
  [System.Xml]System.Xml.Serialization.
  XmlElementAttribute::.ctor(string) =
  ( 01 00 01 52 00 00 ) //"R"
  .get instance float64 Sphere.Sphere::get_Radius()
  .set instance void Sphere.Sphere::set_Radius(float64)
}

Dieser Code definiert eine Eigenschaft namens Radius. Hier finden Sie die Direktiven für die Accessoren auf die Eigenschaft, die die get-und set-Aktionen auf zwei verschiedene Methoden abbilden. Außerdem weist der Code der Eigenschaft ein Attribut zu. (siehe Abschnitt "Attribute hinzufügen") Sehen Sie sich zuerst die beiden Accessor- Methoden der Eigenschaft in Listing3 an.

.method public hidebysig specialname instance float64
  get_Radius() cil managed
  {
    .maxstack 1
    
    ldarg.0
    ldfld float64 Sphere.Sphere::radius
    ret
  }


.method public hidebysig specialname instance void
  set_Radius(float64) cil managed
  {
    .maxstack 2
    ldarg.0
    ldarg.1
    stfld float64 Sphere.Sphere::radius
    ret
  }

Listing 3

Die Methoden get_Radius und set_Radius unterscheiden sich nicht von jeder anderen normalen IL-Methode. Das Einzige, was sie mit der Eigenschaft verbindet, sind die .get- und .set-Direktiven in der Eigenschaftsdeklaration.

Attribute hinzufügen

In ILAsm weisen Sie Attribute an Elemente mithilfe einer .custom-Direktive unmittelbar am Anfang eines Codeblocks hinzu. Das entspricht dem Hinzufügen anderer Direktiven wie .entrypoint, .locals init oder .maxstack.

.custom instance void
  [System.Xml]System.Xml.Serialization.
  XmlElementAttribute::.ctor(string) =
  ( 01 00 01 52 00 00 )

Im obigen Code fügt die .custom-Direktive benutzerdefinierte Metadaten dem Element hinzu. In diesem Fall handelt es sich um eine Instanz eines XmlElementAttribute mit einem Konstruktorargument des Typs string, das den Wert R hat. Das Argument für den Konstruktor muss eine spezielle Kodierung mit besonderen Initialisierungs- und Abschlussbytes haben. Mehr Informationen hierzu finden Sie im Dokument "Partition II Metadata", das in den Quellen am Ende dieses Artikels genannt ist.

Typen instanziieren

Nachdem Sie Ihre Typ-Member definiert haben, ist es an der Zeit, den Typ zu instanziieren. Zuerst müssen Sie lokale Variablen definieren, um die Verweise auf die zu instanziierenden Typen zu speichern.

.locals init (class Sphere.Sphere, 
  class [System.Xml]
  System.Xml.Serialization.XmlSerializer xs,
  class [mscorlib]System.IO.FileStream fs)

Dieser Code erzeugt drei lokale Variablen. Die erste speichert einen Verweis auf das Kugelobjekt (Sphere), die zweite einen Verweis auf einen XmlSerializer und die dritte wird für eine FileStream-Instanz verwendet, die an XmlSerializer zu übergeben ist. Nachdem Sie die lokalen Variablen erzeugt haben, müssen Sie das Objekt instanziieren:

ldc.r8 1.
newobj instance void Sphere.Sphere::.ctor(float64)
stloc.0

Zuerst laden Sie die Argumente für den Konstruktoraufruf auf den Auswertungsstapel und können dann mit der Anweisung newobj eine neue Instanz der Klasse erzeugen, indem Sie die Konstruktormethode angeben, die als Argument des newobj- Aufrufs aufzurufen ist. Der newobj- Aufruf gibt einen Verweis auf die erzeugte Instanz auf dem Auswertungsstapel zurück. Dann brauchen Sie nur noch den Verweis auf eine lokale Variable mithilfe der stloc- Anweisung zu speichern. Der Quellcode für die beiden anderen erforderlichen Objekte sieht wie in Listing 4 aus. Der erste newobj-Codeblock instanziiert den XmlSerializer und der zweite den File- Stream für die Ziel-XML-Datei. Nachdem Sie alle notwendigen Objekte erzeugt haben, können Sie die Serialize-Funktion aufrufen, um die Kugelinformationen dauerhaft in einer XML-Datei zu speichern:

ldloc.1
ldloc.2
ldloc.0
callvirt instance void
  [System.Xml]System.Xml.Serialization.
  XmlSerializer::Serialize(class
  [mscorlib]System.IO.Stream, object)
ldtoken Sphere.Sphere
call class [mscorlib]System.Type
  [mscorlib]System.Type::GetTypeFromHandle
(valuetype
  [mscorlib]System.RuntimeTypeHandle)
ldstr "DevXSample"
newobj instance void
  [System.Xml]System.Xml.Serialization.
  XmlSerializer::.ctor(class [mscorlib]System.Type,
  string)
stloc.1


ldstr "sphere.xml"
ldc.i4.4
newobj instance void
  [mscorlib]System.IO.FileStream::.ctor(string,
  valuetype [mscorlib]System.IO.FileMode)
stloc.2

Listing 4

Dieser Code läd die Verweise auf die Instanzen in der passenden Reihenfolge auf den Auswertungsstapel und verwendet dann callvirt, um die Instanzmethode Serialize aufzurufen, die vom XmlSerializer- Objekt offen gelegt wird. Der Code verwendet üblicherweise callvirt anstelle von call, weil die Serialize-Methode überschrieben wird und die Laufzeit herausfinden muss, welche überschriebene Version in diesem Aufruf zu verwenden ist. Es ist zulässig, eine virtuelle Methode durch call aufzurufen, was anzeigt, dass die Methodenadresse mithilfe der Klasse aufzulösen ist, die durch die Methode spezifiziert ist, anstatt die Adresse dynamisch vom aufzurufenden Objekt aufzulösen. Das können Sie beispielsweise nutzen, um Aufrufe von Methoden auf der als statisch bekannten Basisklasse zu kompilieren.

Zusammenfassung

In dieser dreiteiligen Artikelserie haben Sie die Grundlagen der objektorientierten Programmierung mit Intermediate Language Assembler (ILAsm) kennen gelernt. Obwohl die in dieser Serie beschriebenen Beispiele recht einfach gehalten sind, sollten Sie nach Ihren ersten Schritten relativ leicht Ihren Weg durch die Dokumentation finden.

Links & Literatur

[1] Serge Lidin: Inside Microsoft .NET IL Assembler, MSPress, 2002

[2] Simon Robinson: Advanced .NET Programming, Wrox, 2002

[3] Sehen Sie sich die beiden Dateien im Tool- Developers-Guide-Unterverzeichnis Ihrer SDKInstallation an: Partition I Architecture.doc und Partition III CIL.doc.

Über den Autor

Peter Koen ist ein Allroundtalent für fast alle Bereiche im Microsoft-Entwicklerumfeld. Seit September 2004 arbeitet Peter als Technologieberater bei Microsoft Österreich und hat die SQL Server User Group in Österreich gegründet.


Anzeigen: