(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Verwalteter Code hinter den Kulissen, Teil 1

Veröffentlicht: 31. Aug 2005
Von Peter Koen

Es spielt keine Rolle, mit welcher Hochsprache Sie Ihre .NET-Anwendungen erstellen. Alle werden in die Intermediate Language (IL) kompiliert. In dieser Reihe steigen Sie in das .NET Framework ein und erfahren, wie IL arbeitet. Erstellen Sie .NET-Assemblies direkt in IL ohne Umwege über eine Hochsprache oder ausgefeilte Entwicklungsumgebungen.

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

Es gibt viele Gründe, den Intermediate Language Assembler (kurz ILAsm) anstelle einer Hochsprache wie C# oder VB .NET zu verwenden. Beispielsweise lassen sich mit ILAsm Dinge realisieren, welche die gewählte Hochsprache nicht unterstützt, oder Sie können Fehler in einer Assembly beseitigen, ohne dass der Quellcode vorliegen muss (natürlich nur, wenn er nicht signiert ist). Vielleicht sind Sie aber auch nur neugierig, was tatsächlich hinter den Kulissen abläuft.

Bevor es um IL selbst geht, brauchen Sie ein paar Kenntnisse zu den Technologien, die dem .NET-Ausführungsmodul zugrunde liegen. Die Erläuterungen sind kurz und knapp gehalten; für Details sei auf die am Ende angegebenen Quellen verwiesen. Dieser Artikel setzt voraus, dass Sie mit der .NET-Programmierung im Allgemeinen bewandert sind. Es spielt dabei keine Rolle, mit welcher Sprache Sie Ihre .NET-Anwendungen schreiben, dieser Artikel gilt für jede beliebige .NET-Assembly. Um die Beispiele nachzuvollziehen, benötigen Sie das Microsoft .NET SDK und einen Editor für reinen Text (Notepad genügt hier vollauf).

Auf dieser Seite

 CLI - Common Language Infrastructure
 Common Type System (CTS)
 Metadaten
 Common Language Specification (CLS)
 Virtual Execution System (VES)
 Wie das VES funktioniert
 Methodenzustand
 Auswertungs-Stack
 Hello World - die Tools
 Hello World - der Code
 Hello World - eine Erklärung
 Fehlt noch etwas?

CLI - Common Language Infrastructure

Die Spezifikation CLI definiert, wie ausführbarer Code (in diesem Fall Common Intermediate Language, kurz CIL) in einer Ausführungsumgebung (dem Virtual Execution System von .NET, kurz VES) abläuft. Die CLI besteht aus vier Teilen:

  • Allgemeines Typsystem (Common Type System, CTS)

  • Metadaten

  • Allgemeine Sprachspezifikation (Common Language Specification, CLS)

  • Virtuelles Ausführungssystem (Virtual Execution System)

Die folgenden Abschnitte beschreiben diese vier wesentlichen Teile im Überblick.

Common Type System (CTS)

Das allgemeine Typsystem bildet den zentralen Bestandteil der CLI. Es beschreibt, welche Typen für alle CLI-kompatiblen Hochsprachen verfügbar sind - die Typen lassen sich von einem Compiler und von der CLI selbst verwenden. Das Common Type System garantiert vor allem die Typsicherheit. Diese stellt im Grunde drei Axiome auf, die für jeden Typ in der CLI gelten müssen:

  • Verweise sind, was sie von sich behaupten. Jeder Verweis auf ein Objekt muss mit genügend Typinformationen sicherstellen, dass es sich um das Objekt handelt, was es zu sein scheint. Verweise dürfen beispielsweise nicht auf Zeichendaten in der einen Codezeile zeigen und in einer anderen Codezeile als Integer-Daten interpretiert werden. Jeder Verweis ist typisiert und enthält auch Informationen über die möglichen Umwandlungen für diesen Typ. Die Typsicherheit verhindert Fehler oder Sicherheitsverletzungen durch falsche Interpretation von referenzierten Daten.

  • Identitäten sind, wer sie behaupten zu sein. Diese Regel verhindert die böswillige Verwendung eines Objekts, indem der Compiler so manipuliert wird, dass er einen anderen Typ oder eine andere Sicherheitsdomäne verwendet. Das Objekt ist ausschließlich über Funktionen oder Felder zugänglich, die durch seine Typinformationen ausgewiesen sind. Dennoch ist Vorsicht angebracht, da sich eine Klasse so entwerfen lässt, dass sie die Sicherheit gefährdet.

  • Es lassen sich nur angemessen definierte Operationen aufrufen. Zugängliche Funktionen und Felder werden durch den Typ des Verweises definiert, der auf das Objekt zeigt. Das berücksichtigt bereits die Sichtbarkeit, die von Zugriffsmodifizierern stammt. Beispielsweise sind private Felder nur in der Klasse selbst und nicht außerhalb sichtbar.

Metadaten

Metadaten speichern Informationen über Typen in einer solchen Form, dass jedes CLI-kompatible Tool (Compiler, Debugger oder auch das Ausführungssystem) diese Daten lesen kann, unabhängig davon, mit welcher Programmiersprache sie erzeugt wurden. Metadaten werden hauptsächlich für die deklarative Programmierung verwendet. Informationen wie Debugger-Sichtbarkeit werden in den Typ-Metadaten gespeichert, anstatt für diesen Zweck eine spezielle Schnittstelle abzuleiten. Für den Programmierer sind Metadaten sowohl bequemer als auch einfacher zu verwenden. Die Ausführungsumgebung kann mithilfe von Metadaten unterschiedliche Ausführungsmodelle unterstützen, Optimierungsregeln besser steuern und automatisierte Unterstützung für integrierte Dienste bereitstellen. Außerdem erweisen sich Metadaten als hilfreich für die Weitergabe von Anwendungen, was vor allem dann gilt, wenn unterschiedliche Zielumgebungen zu unterstützen sind.

Common Language Specification (CLS)

Die CLS ist eine Art Vertrag zwischen der CLI und dem Sprachdesigner. Gäbe es keine allgemeine Spezifikation, müsste jede höhere Programmiersprache mit einer anderen .NET-Basisklassenbibliothek ausgeliefert werden. Mit der CLS dagegen halten sich alle Sprachen, die Code für die CLI kompilieren können, an eine festgelegte Menge von Funktionen und Typen, die eine gemeinsame Basis von Diensten bilden.

Virtual Execution System (VES)

Das VES setzt das CTS durch und führt verwalteten Code aus - prinzipiell sind verwaltete Daten nichts weiter als Daten, die die CLI über die automatische Speicherverwaltung - die Garbage Collection - lädt und entlädt. Verwalteter Code ist der Code, der auf diese Daten zugreifen kann. Und weil die CLI die Daten verwaltet, gilt analog, dass kein nicht-CLI-fähiger Code auf die Daten zugreifen kann. Verwalteter Code kann allerdings auch auf nicht verwaltete Daten zugreifen, weil diese durch keinen speziellen Mechanismus gegen Zugriffe geschützt sind. Außerdem kümmert sich die CLI um die Ausnahmebehandlung sowie das Speichern und Abrufen von Sicherheitsinformationen in verwaltetem Code.

Wie das VES funktioniert

Das VES muss in der Lage sein, verwalteten Code auf vielen möglichen Plattformen auszuführen. Deshalb ist es unmöglich, das VES wie eine Ausführungsumgebung aufzubauen, die für eine konkrete Prozessorarchitektur - beispielsweise die Intel-Architektur - ausgelegt ist. Das bedeutet, dass die CLI keine Annahmen über verfügbare Register, Cache oder Operationen auf dem Zielsystem treffen kann. Alle diese Betrachtungen werden in eine für alle Plattformen gemeinsame Form oder in ein theoretisches Format abstrahiert, das sich für die konkrete Zielplattform durch den Just-In-Time-Compiler (JIT-Compiler) kompilieren lässt, wenn der Code später ausgeführt wird.

Methodenzustand

Das .NET VES implementiert für diesen Zweck einige interessante Mechanismen. Das Ausführungsmodul führt alle Steuerungs- Threads der Anwendungen mit verwaltetem Code und die Speicherverwaltung in einem gemeinsam genutzten Speicherraum aus. Die von jedem Steuerungs- Thread aufgerufenen Methoden legen ihre Informationen auf einem verwalteten Heap ab, um den sich die automatische Speicherverwaltung kümmert. Das VES erzeugt mit jedem Methodenaufruf einen neuen Speicherblock für den Methodenzustand. Wenn der letzte Methodenzustand (für den Einsprungpunkt einer Anwendung) freigegeben wird, terminiert die Anwendung. Abbildung 1 (die auf einer Darstellung im "Tools Developer's Guide" von Microsoft basiert) zeigt den Ablauf von Methodenaufrufen im VES. Ist eine Methode fertig abgearbeitet, übergibt das Ausführungsmodul die Steuerung an den Methodenzustand zurück, der die abgearbeitete Methode aufgerufen hat.

Zustandsmodell: Steuerungs-Threads legen Methodenzustände auf dem verwalteten Heap an
Abb. 1. Zustandsmodell: Steuerungs-Threads legen Methodenzustände auf dem verwalteten Heap an

Ein Methodenzustand besteht immer aus mehreren Teilen:

  • Befehlszeiger (Instruction Pointer) - zeigt auf die nächste auszuführende Anweisung

  • Methodenbeschreibung (Method Info Handle) - speichert schreibgeschützte Informationen über die Methodensignatur

  • Auswertungs-Stack (Evaluation Stack) - mehr dazu später

  • Eingabeparameter (Incoming Arguments) - die beim Aufruf dieser Methode zu verwendenden Argumente

  • Lokale Variablen - ein nullbasiertes Array aller lokalen Objekte

  • Lokale Zuordnungen (Local Allocations) - für die dynamische Zuordnung von lokalen Objekten verwendet

  • Sicherheitsbeschreibung (Security Descriptor) - durch verwalteten Code nicht zugänglich, aber von der CLI verwendet, um überschriebene Sicherheitseinstellungen (Assertion, das Gewähren und Verweigern) aufzuzeichnen

  • Rückgabezustand (Return State Handle) - stellt den Methodenzustand auf den Zustand vom Aufrufer wieder her (auch als dynamische Verknüpfung bezeichnet)

Auswertungs-Stack

Jeder Methodenzustand besitzt einen zugehörigen Auswertungs-Stack, auf den die meisten CLI-Anweisungen zurückgreifen, um Argumente für Aufrufe abzuholen und Ergebnisse von Aufrufen abzulegen. Der Auswertungs-Stack besteht nur aus Objekten; es spielt keine Rolle, ob Sie eine Ganzzahl, einen String oder ein benutzerdefiniertes Objekt auf den Stack legen - die virtuelle Umgebung registriert nur, wie viele Einträge auf dem Stack gespeichert sind und in welcher Reihenfolge. Natürlich ist das VES um die Typsicherheit besorgt, wenn es Methoden aufruft, doch momentan müssen Sie nur wissen, dass Sie die Argumente für eine Methode auf dem Auswertungs- Stack ablegen müssen, bevor Sie die Methode aufrufen. Dieses Muster gilt für jede CLI-Funktion, die Argumente verlangt. Des Weiteren darf der Auswertungs- Stack an jedem möglichen Austrittspunkt einer Methode nur den Rückgabewert enthalten (oder keinen Wert, wenn eine Methode void zurückgibt - dies entspricht einer Sub-Methode in VB.NET).

Hello World - die Tools

Genug der grauen IL-Theorie. Damit Sie das erste "Hello-World"-Beispiel nachvollziehen können, brauchen Sie:

  • Ilasm.exe ist der IL-Code-Compiler. Er befindet sich im Windows-Verzeichnis unter %windir%/Microsoft.NET/Framework//ilasm.exe.

  • Notepad.exe ist der Standardeditor von Microsoft; allerdings können Sie jeden Editor Ihrer Wahl verwenden, sofern er reinen Text (ohne Formatierungszeichen) erzeugt.

  • Peverify.exe dient dazu, die resultierende Assembly zu verifizieren. Dieses Tool brauchen Sie, um Fehler im generierten IL-Bytecode aufzuspüren, beispielsweise Fehler der Typsicherheit. Es befindet sich im bin-Verzeichnis der SDK-Installation.

  • Ildasm.exe ist zwar eigentlich nicht für die Programmierung der Microsoft Intermediate Language (MSIL) erforderlich, erweist sich aber als nützlich, wenn Sie IL-Code aus bereits kompilierten Assemblies inspizieren möchten. Dieses Tool finden Sie im bin-Verzeichnis der SDKInstallation.

Um sich die Arbeit zu erleichtern, sollten Sie diese Verzeichnisse in die Umgebungsvariable PATHaufnehmen. Falls Sie Visual Studio .NET installiert haben, können Sie alternativ auch jene Eingabeaufforderung verwenden, die über das Menü EXTRAS von Visual Studio verfügbar ist.

Hello World - der Code

Das folgende Beispiel zeigt, wie Sie die typische "Hello-World"-Anwendung mit ILAsm kodieren. Um das Beispiel möglichst einfach zu halten, wurde alles weggelassen, was nicht unbedingt notwendig ist. Starten Sie Ihren bevorzugten Editor, geben Sie den folgenden Code ein und speichern Sie den Inhalt in der Datei Helloword. il. Kompilieren Sie die Datei mit Ilasm.exe mit einer Befehlszeile wie zum Beispiel c:\>ilasm.exe helloworld.il. Dieser Befehl erzeugt die ausführbare Datei Helloworld.exe.

.assembly HelloWorld {}

.method static void Main() cil managed
{
  .entrypoint
  .maxstack 1


  ldstr "Hello World"
  call void
  [mscorlib]System.Console::WriteLine(string)
  ret
}

Um zu überprüfen, ob Ihr Code und die Metadaten allen Regeln der CLI genügen, müssen Sie die ausführbare Datei verifizieren. Führen Sie dazu den Befehl Peverify. exe helloworld.exe aus. Wenn PEVerify feststellt, dass alle Ihre Klassen und Methoden verifiziert wurden, können Sie die ausführbare Datei starten.

Hello World - eine Erklärung

Alles, was mit einem Punkt beginnt, ist eine Direktive (Anweisung) an den Compiler. Zuerst weist der Code den Compiler an, eine Assembly namens HelloWorld zu erzeugen. Die beiden geschweiften Klammern nach der Assembly können Metadaten über diese Assembly enthalten, z.B. eine Versionsnummer. Der hier wiedergegebene Code lässt die Metadaten aus, um den Code so einfach wie möglich zu halten. Der Compiler erzeugt die Standardversionsnummer 0:0:0:0, sofern Sie nichts anderes angeben. Außerdem fügt der Compiler die erforderliche Unterstützungsdirektive für die mscorlib hinzu. Wenn Sie eine bestimmte mscorlib-Version verwenden müssen, können Sie das explizit im Code angeben. Momentan aber sollten Sie möglichst die Standardwerte übernehmen. Alle am Beginn der Datei eingetragenen Metadaten gehören zum sog. Assembly-Manifest. Darüber hinaus lassen sich andere Metadaten wie z.B. referenzierte Assemblies, das Subsystem (ausführbare Windows-Datei, ausführbare Konsolenanwendung, Bibliothek), der Modulname usw. hinzufügen. Die nächste Zeile ist eine Methodendirektive:

.method static void Main() cil managed

Die Methodendirektive weist den Compiler an, einen neuen Methodenzustand zu erzeugen. Daran schließt sich die Signatur dieser Methode an. Dieses Beispiel umfasst sechs Teile:

  • Der Modifizierer static spezifiziert, dass diese Methode ohne eine Instanz einer Klasse aufgerufen werden kann.

  • void sagt dem Compiler, dass es keinenRückgabewert gibt.

  • Main ist der Name der Methode. Für das Beispiel wurde dieser Name gewählt, weil er auch der Name für den Einsprungpunkt von C#-Anwendungen ist. Allerdings können Sie auch einen anderen Namen verwenden.

  • Die leeren Klammern () zeigen an, dass diese Methode keine Argumente übernimmt.

  • cil definiert die Methode als CIL-Methode.

  • 0managed ist einfach das Schlüsselwort für verwalteten Code und bedeutet, dass die Methode keinen Zugriff auf nicht verwaltete Daten hat.

Der Rumpf der Methode ist genau wie bei C# in geschweifte Klammern eingefasst. Die Klammern definieren den Gültigkeitsbereich der Methode. Im Rumpf der Methode finden Sie zwei weitere Direktiven:

  • .entrypoint weist den Compiler an, diese Methode als Einsprungpunkt des Moduls zu markieren. Beim Start des Moduls ruft das Programm zuerst diese Methode auf. Die Anwendung terminiert, wenn diese Methode zurückkehrt.

  • Mit .maxstack 1 bestimmt der Compiler die Größe des Auswertungs-Stack, der für die Ausführung dieser Methode erforderlich ist. Die Größe wird in abstrakten Elementen und nicht in Bytes gezählt, weil dieser Wert nicht zur Laufzeit gebraucht wird, sondern nur dazu da ist, die Verifizierbarkeit für den Compiler von CIL zu systemeigenem Code bereitzustellen.

Damit kennen Sie alle verwendeten Direktiven und können sich dem Code für den Nachrichtenrumpf zuwenden, der ziemlich einfach und unkompliziert ist:

Die Anweisung ldstr "Hello World" legt den String auf dem Auswertungs- Stack ab. Der Token ldstr steht (natürlich) für "Load String". Die Anweisung

call void [mscorlib]System.Console::WriteLine(string)

ruft die Methode WriteLine der Klasse System.Console auf, die sich in der Assembly mscorlib befindet. Die Syntax für Methodenaufrufe ist immer die gleiche: der Assembly-Name in eckigen Klammern, gefolgt vom voll gekennzeichneten Klassennamen (eine Kurzversion wie in höheren Programmiersprachen ist nicht zulässig), zwei Doppelpunkten, dem Methodennamen und der Liste der Argumenttypen. Diese Anweisung holt den zuvor geladenen String vom Stack und übergibt ihn an die Methode WriteLine.

Die Anweisung ret gibt die Programmsteuerung von der Methode an den Aufrufer zurück. An diesem Punkt muss der Auswertungs-Stack leer sein, weil die Methode void zurückgibt. Die Methode im Beispiel ist sehr einfach gehalten. Es sollte kein Problem geben, die Elemente auf dem Auswertungs-Stack zu berechnen. Bei einer größeren Methode empfiehlt es sich, StackÜbergangsdiagramme zu verwenden. Derartige Diagramme sind leicht zu lesen und bieten hilfreiche Informationen über den aktuellen Zustand des Auswertungs-Stack. Ein einfaches Stack-Übergangsdiagramm sieht folgendermaßen aus:

..., old state --> ..., new state

Die Auslassungszeichen (...) repräsentieren den Zustand des Stack von anderen Operationen, wenn für das eigentliche Diagramm keine derartigen Informationen relevant sind. Beispielsweise sehen drei aufeinander folgende ldstr-Befehle in einem Stack-Übergangsdiagramm so aus:

<empty> --> value1
value1 --> value1, value2
..., value2 --> ..., value2, value3

Das Diagramm für ldstr besitzt das folgende Aussehen:

<empty> --> "Hello World"

Der Methodenaufruf besitzt das folgende Diagramm:

"Hello, World" --> <empty>

Fehlt noch etwas?

Wenn Sie ein C#-Programmierer sind oder eine andere rein objektorientierte Sprache einsetzen, haben Sie vielleicht bemerkt, dass es in diesem Beispiel keine Klassendirektive gibt. IL braucht keine Klasse und lässt sich wie eine prozedurale Programmiersprache verwenden. Beispielsweise erzwingt C# die Objektorientierung (d.h. fordert eine Klasse), während VB.NET-Programmierer keine Klasse explizit mit einer Main-Methode als Einsprungspunkt erzeugen müssen. Diese Einschränkungen sind Regeln, die jede höhere Programmiersprache selbst aufstellt. Es ist einzig und allein zu beachten, dass ILAsm eine Methode benötigt, die den Einsprungspunkt spezifiziert.

Peter Koen ist ein Allroundtalent für fast alle Bereiche im Microsoft-Entwicklerumfeld. Er war bis vor kurzem Microsoft MVP und unabhängiger Berater, Autor und Programmierer. Seit September 2004 arbeitet Peter als Technologieberater bei Microsoft Österreich. Außerdem hat Peter die SQL Server User Group in Österreich gegründet.


Anzeigen:
© 2014 Microsoft