Laden...

[Artikel] NET Runtime, Kapitel 1: Laden und Ausführen

Erstellt von egrath vor 17 Jahren Letzter Beitrag vor 17 Jahren 33.740 Views
egrath Themenstarter:in
871 Beiträge seit 2005
vor 17 Jahren
[Artikel] NET Runtime, Kapitel 1: Laden und Ausführen

Microsoft.NET Framework Runtime, Teil 1: Die ausführung von Assemblys

Diese Artikelreihe soll einen Grundlegenden Überblick über die .NET Runtime geben. Dabei soll nicht nur die oberfläche betrachtet sondern auch ein klein wenig davon gezeigt werden wie es "unter dem Lack" aussieht.

Das Laden der Applikation

Wenn eine Applikation gestartet wird, überprüft das Betriebssystem (genauer gesagt der Loader) um welchen Typ einer ausführbaren Datei es sich handelt. Dies wird über den Header der Datei ermittelt, welcher im sg. PE (Portable Executable) Format am Beginn der Datei angesiedelt ist. Im Fall einer .NET Executable ist im Header angegeben dass sich der Eintrittspunkt der Applikation nicht in der aktuellen Datei befindet, sondern in der "mscoree.dll" - diese ist anschliessend für das laden und ausführen des enthaltenen CIL Codes zuständig. Dazu aber später mehr.


Abb 1.: Das .NET Framework ist nicht installiert, der Loader beschwert sich

Überprüfen ob es sich um eine .NET Executable kann man mittels des beim Visual Studio mitgelieferten Tools "dumpbin" welches so ziemlich alle Informationen von den diversen Microsoft Executables anzeigen kann. Für .NET Programmierer sicherlich am interessantesten ist der Switch "/CLRHEADER" mit welchem unter anderem Informationen über die Runtime Version angezeigt wird mit welcher die Applikation erstellt wurde:


Dump of file hello_11.exe

File Type: EXECUTABLE IMAGE

  clr Header:

              48 cb
            2.00 runtime version
            2080 [     258] RVA [size] of MetaData Directory
               1 flags
         6000001 entry point token
               0 [       0] RVA [size] of Resources Directory
               0 [       0] RVA [size] of StrongNameSignature Directory
               0 [       0] RVA [size] of CodeManagerTable Directory
               0 [       0] RVA [size] of VTableFixups Directory
               0 [       0] RVA [size] of ExportAddressTableJumps Directory
               0 [       0] RVA [size] of ManagedNativeHeader Directory


  Summary

        2000 .reloc
        2000 .rsrc
        2000 .text

Der interessante Teil hierbei ist die "runtime version" welche angiebt mit welcher Framework Version die Datei erstellt wurde. Diese kann folgende Werte haben:*2.00: Framework Version 1.0 und 1.1 *2.5: Framework Version 2.0

Warum Version 1.0 und 1.1 die gleiche Runtime Version haben resultiert daraus, dass sich bei diesen beiden Versionen nichts an der Runtime selbst geändert hat (Keine neuen IL Befehle) - erst mit Version 2.0 wurden neue IL Befehle eingeführt. Nichts desto trotz kann eine 1.0 Runtime keine 1.1 oder gar 2.0 Assemblys ausführen, da die abhängigkeiten zu den externen Referenzen nicht gegeben sind (Assemblys sind stark signiert, d.h. eine 1.1 Applikation referenziert beispielsweise auf die mscorlib.dll in Version 1.1). Natürlich funktioniert aber der andere Weg rum.

Sollte man allerdings eine Applikation welche beispielsweise unter 1.1 entwickelt ist so einschränken wollen dass diese unter 2.0 nicht läuft (weil es vielleicht einen Fehler gibt der in Version 2.0 auftritt) so kann man dies mittels einer .config Datei erledigen. Diese hat den gleichen Namen wie die ausführbare Datei, nur mit einem ".config" als Erweiterung. In dieser kann festgelegt werden, welche Runtime die Datei benötigt und welche sie unterstützt. Eine solche Datei kann beispielsweise folgendermassen aussehen:


<configuration>
    <startup>
        <requiredRuntime version="1.0.3705"/>
        <supportedRuntime version="1.1.4322"/>
        <supportedRuntime version="1.0.3705"/>
    </startup>
</configuration>

Obiges Beispiel konfiguriert eine Applikation so, dass diese nur auf dem 1.0 und 1.1 Framework läuft. 2.0 wird nicht unterstützt. Sollte auf dem ausführenden System kein Framework in der unterstützten Version vorhanden sein, so wird die ausführung abgebrochen.

Bedeutung der Elemente:


Elementname         |   Parameter       |  Bedeutung
--------------------+-------------------+-------------
requiredRuntime     |   Versionsnummer  |  Wird nur vom Framework 1.0 benutzt. Gibt die kleinste Version an welche zur Ausführung benutzt werden kann
supportedRuntime    |   Versionsnummer  |  Gibt an, welche Runtime die Applikation ausführen kann.


Abb 2.: Der Versuch eine 1.1 Applikation gezwungen auf einem 1.0 System auszuführen schlägt fehl.

Das Ausführen der Applikation

Schematisch gesehen funktioniert die Ausführung folgendermassen:


Abb 3.: .NET Ausführungsmodell

Interessant wird die Ausführung einer Applikation erst dann, wenn man sich auf einem 64-Bit System (inkl. 64-Bit Betriebssystem, bsp Windows XP x64 Edition oder Windows 2003 Server ITANIUM Edition) befindet. Dann muss nämlich noch zusätzlich entschieden werden, welche Runtime verwendet wird (32 od. 64 Bit). Dies geschieht auf folgendem Weg:

Die Angabe auf welcher Platform (x86,x64,IA64,Any) die Assembly laufen soll kann bei der Kompilierung mittels des Switches "/platform" angegeben werden. Gültige Ziele sind "x86", "Itanium" (f. IA-64), "x64" und "anycpu". Im Fall dass eine Assembly für eine spezifische CPU gebunden ist wird beim laden auf einer ungültigen Plattform eine Exception vom Typ "BadImageFormat" erzeugt.

Ein sehr wichtiger Punkt welchen es zu beachten gibt ist, dass in einen 64 Bit Prozess keine 32 Bit DLL's geladen werden können. Auch Platform/Invoke's oder COM nach 32 Bit sind verboten und führen zwangsläufig immer zu einer Exception.

Erwähnenswert ist in diesem Zusammenhang das Tool "CorFlags.exe" welches mit dem SDK mitinstalliert wird und mit welchem sich Header-Informationen über eine Assembly anzeigen lassen. Eine typische Ausgabe sieht beispielsweise so aus:


Version: v1.1.4322
CLR Header: 2.0
PE: PE32
CorFlags: 1
ILONLY: 1
32BIT: 0
Signed: 0

Mit dem Tool ist es möglich, die oben gelisteten Parameter zu verändern. Somit kann man beispielsweise bei properitären Applikationen erzwingen dass diese mittels einer eventuell installierten 64 BIT Runtime ausgeführt werden auch wenn der Hersteller dies nicht vorgesehen hat und das Flag "32BIT" auf True gesetzt hat. Weiters ist es damit möglich eine 1.1 Applikation in einer 64 BIT Runtime ausführen zu lassen indem man den Header der Executable aktualisiert:

corflags /UpgradeCLRHeader <Assembly>

Untenstehend eine kurze Zusammenfassung der einzelnen Flags:


Flagname    |   Bedeutung
------------+-----------------------
CLR Header  |   Die Version des CLR Headers (2.0 od 2.5; siehe oben)
ILONLY      |   .NET Assemblys dürfen neben IL Code auch noch nativen Maschinencode enthalten. Dies wird allerdings von den gängigen Compilern (bsp. csc und vbc) nicht benutzt und kann dementsprechend vernachlässigt werden.
32BIT       |   Obwohl IL Code normalerweise Bitbreiten-unabhängig ist, kann mit diesem Flag angegeben werden dass der Code nur auf 32 BIT Runtimes ausgeführt werden darf. (Da beispielsweise mit nativem 32 BIT Code interagiert wird)
SIGNED      |   Gibt an ob die Assembly digital signiert ist
PE          |   Welche PE Header Version wird benutzt? (PE32 oder PE32+; PE32+ wird bei 64 BIT Executables verwendet)

Der Global Assembly Cache (GAC) und Native Images

Der GAC ist ein zentraler Bestandteil des Frameworks. Seine Aufgabe besteht grob gesehen darin, Assemblys welche von mehreren Applikationen benutzt werden zentral zu speichern. Da .NET Assemblys versioniert sind, kann eine beliebige Anzahl von gleichen Assemblys im Cache liegen sofern diese unterschiedliche Versionsnummern haben.

Im Normalfall werden Assemblys automatisiert vom jeweiligen Installationsprogramm der Applikation in den GAC "installiert". Dies geschieht durch den Aufruf des Tools "gacutil.exe" mit den entsprechenden Parametern. Folgende Parameter werden häufig benutzt:


Option | Parameter    | Bedeutung
-------+--------------+-------------------------
-l     |              | Zeigt alle im GAC installierten Assemblys und deren Versionsnummer
-i     |  <Assembly>  | Installiert die durch <Assembly> angegebene Assembly in den GAC. Es ist zu beachten dass diese Assembly einen Strong Name haben muss (und eine Version haben sollte)

Der Global Assembly Cache liegt physikalisch im Filesystem unter " %SYSTEMROOT%\Assembly\GAC"

Ein weitere wichtiger Bestandteil sind die sg. "Nativen Images". Wie man vorab im Diagramm gesehen hat, werden Assemblys nicht interpretiert ausgeführt sondern zu nativen Maschinencode der jeweiligen Plattform kompiliert (siehe Nachfolgenden Absatz) und erst anschliessend ausgeführt. Sind für die auszuführende Assembly keine nativen Images verfügbar so werden diese temporär zur Laufzeit erzeugt. Dies kann bei grossen Applikationen zu einer gewissen Verzögerung im start führen. Für Applikationen welche beim Endbenutzer installiert werden ist es deshalb sinnvoll diese schon vom Installer in native Images zu kompilieren.

Dies geschieht mittels des Tools "ngen.exe". Folgende Parameter sind gebräuchlich:


Option      Parameter   Bedeutung
----------+-----------+------------------------------
install   |<Assembly> | Kompiliert die durch <Assembly> angegebene Assembly in nativen Code und installiert diese im Native Image Cache
uninstall |<Assembly> |	Entfernt das Kompilat der durch <Assembly> angegebenen Assembly aus dem Native Image Cache
display   |           | Zeigt alle im Native Image Cache liegenden, kompilierten Assemblys

Der Native Image Cache liegt physikalisch im Filesystem unter "%SYSTEMROOT%\Assembly\NativeImages_<RuntimeVersion>[<SysArch>]

Der JIT Compiler

Um das übersetzen von IL Code nach für die Plattform nativen Maschinencode kümmert sich der JIT Compiler. JIT steht dabei für "Just-In-Time" was soviel bedeutet als dass Code erst zur Ausführungszeit übersetzt wird (was vorausgesetzt man benutzt keine NGEN-Prekompilierten Images) auch richtig ist. Wie bekannt bestehen Assemblys aus sg. IL (Intermediate Language) Code welcher nicht direkt ausgeführt werden kann, da die CPU eines Systems nur ihre nativen Befehle und Operationen kennt (welche für einen High-Level Programmierer sicherlich sehr rudimentär wirken)

Schematisch gesehen sieht der Vorgang des JIT'ens so aus:


Abb 4.: Kompiliervorgang User/Runtime

Der JIT Vorgang ist durch den grünen Hintergrund dargestellt. Die Frontend Kompilation ist nur aus verständlichkeitsgründen angeführt. Wenn man das Bild im ganzen Betrachtet so sehen wir dass zwei Compiler zum einsatz kommen. Der Frontend Compiler übersetzt die Hochsprache nach IL (also bsp. C# nach IL). Der Backend Compiler (JIT) anschliessend von IL nach Maschinencode (also bsp. von IL nach x86). Zusätzlich zu beachten ist dass der JIT Compiler beim Applikationsstart nicht den gesamten IL Code der Assembly übersetzt sondern nur jene Klassen welche tatsächlich benötigt werden. Aus diesem Grund kann es vorkommen, dass es während der ausführung dazu kommt dass der JIT weitere Klassen kompilieren muss - dies sollte in den meisten Fällen allerdings ohne merkbare Einbussen von statten gehen.

Ein Beispiel für die Kompilierung von IL nach x86:


Abb 5.: IL Code eines "Hello, World!" Programms

Das Ergebnis der Kompilierung ist reiner x86 Maschinencode welcher beispielsweise folgendermassen aussieht:


Abb 6.: x86 Maschinencode

Maschinengenerierter Code ist meist selbst für personen welche über ausreichende Assembler Kenntnisse verfügen nur schwer lesbar - deshalb und da es nicht dem direkten Verständnis der .NET Runtime beiträgt werden wir dies nicht weiter vertiefen.

4.506 Beiträge seit 2004
vor 17 Jahren

Hallo egrath,

richtig cooler Artikel! Respekt und 👍 Daumen hoch!

Gruß
Norman-Timo

A: “Wie ist denn das Wetter bei euch?”
B: “Caps Lock.”
A: “Hä?”
B: “Na ja, Shift ohne Ende!”

1.130 Beiträge seit 2005
vor 17 Jahren

Schöner Artikel, vielen Dank dafür 👍

L
4 Beiträge seit 2006
vor 17 Jahren

Toller Artikel. Selten so konzentriert soviel Hintergrundwissen auf einem Fleck gesehen.
Respekt. Ist gleich in meine Bookmarks gewandert 🙂 👍

greets lämpi

Oh Herr, lass mein Office schneller automatisch abspeichern als mein Kernel abschmieren kann... Na gut.Dann gib mir wenigstens unendliche Geduld, unerschütterlichen Gleichmut und ein Erinnerungsvermögen wie ein Elefant...

228 Beiträge seit 2006
vor 17 Jahren

ich muss auch sagen gefällt mir echt gut 👍 👍 👍
grüße MEt45

Medieval Fantasy Online - ORPG Projekt
.NET - Try and Error - Blog - Gemeinschaftsblog
MEt45's Dev Garage - Eigener Blog