Ausarbeitung

Werbung
DYNAMIK AUF DER VM
Unterstützung dynamischer Sprachen auf der Java Virtual Machine
Marcus Rieche
für jedes Programm eine eigene virtuelle Umgebung
ausführen und der Virtual Machine selbst nur wenig
Rechte vom Betriebssystem her geben. Dadurch
können sich Programme weder untereinander noch
dem System schaden, abgesehen von legitimen
Zugriffen. z.B. dem löschen von Dateien innerhalb
eines nicht System kritischen Bereiches.
ABSTRACT
Die JVM ist ein interessanter Ansatz, um die
Portierung
von
Programmen
auf
unterschiedliche Hardware Plattformen durch
zu führen. Die JVM ist dabei auf Java optimiert.
Die Programmiersprache Java ist jedoch nicht
für alle Problemstellungen die beste Wahl,
womit sich der Wunsch ergibt auch andere
Sprachen auf der JVM zu unterstützen.
Insbesondere dynamische Sprachen erfreuen
sich derzeit großer Beliebtheit und daher wird
im folgenden betrachtet, was man machen
könnte und müsste, um diese auf der JVM zu
unterstützen.
1.2.
Dynamik kann man auch dadurch erreichen, dass
man Verknüpfungen im Stack und im Object Tree
frei ändern kann. Dazu muss lediglich eine API
bereitgestellt werden, welche diese Änderungen
unterstützt.
1. EINLEITUNG
Das würde aber bedeuten, dass es von da an keine
Garantien mehr gibt. Es gäbe keinen Code
Abschnitt mehr, welcher nicht verändert werden
könnte.
Das
wäre
für
Geschäftskritische
Anwendungen,
wie
z.B.
Bankingoder
Abrechnungsysteme
nicht
unbedingt
wünschenswert. Punktuell können dort dynamische
Elemente gewünscht sein. z.B. für die
Berechnungen von Preisen. Sollte Schadcode an
dieser Stelle ausgeführt werden, wird dieses später
wohl durch Null oder Negativ Zeilen in der
Datenbank auffällig werden. Freie Änderungen
könnten nach Auslieferung der Ware zu einer
Löschung des Lieferscheines führen, womit es dann
auch keine Spuren mehr gäbe.
Bevor Anpassungen der JVM betrachtet werden,
werden kurz zwei andere Ansätze angedeutet, um
den
Vorteil
des
gewählten
Implementierungsansatzes zu verdeutlichen.
1.1.
FREIER ZUGRIFF AUF DIE
REFLECTION INFORMATIONEN
REGISTER BASIERTE VM
Es gibt bereits Implementierungen, welche eine
Register basierte Virtual Machine umsetzen. Diese
Maschinen stellen ein vollständiges Assembler
bereit. Es wird also ein idealisierter PC im PC
ausgeführt.
Mit
Hilfe
der
Assembler
Implementierung lassen sich alle Probleme genau so
wie auf dem Hardware Vorbild lösen.
Außer dem Schadcode Szenario können sich auch
von Entwickler Seite her leichter Fehler
einschleichen, wenn es keine Kontrolle über
veränderbare Punkte gibt. Es könnten z.B.
Querabhängigkeiten bestehen.
Problematisch
könnten
allerdings
die
Sicherheitsmechanismen sein. Die Entwicklung der
Betriebssysteme zeigt, dass man relativ einfach
Systeme entwickeln kann, die viele Freiheiten
gewähren und man dann Konzepte etablieren muss,
um diese Freiheiten wieder einzuschränken. Als
Beispiel seien genannt der Buffer Overflow
Schutz ,die Isolierung von Prozessen und Pakete
wie SELinux. Das diese Mechanismen erst in den
letzten Jahren zum Standard wurden, zeigt das bei
der Implementierung eines auf Registern basierten
Systems sich diese Sicherheitsaspekte nicht sofort
erschließen.
1.3.
MOTIVATION
Beiden Ansätzen ist gemein, dass sie zu viele
Freiheitsgrade bieten. Weiterhin ist die Java Virtual
Machine bereits Praxis erprobt und daher ist es
sinnvoll Punktuelle Erweiterungen vorzunehmen,
ohne das Gesamtkonzept zu gefährden.
Für eine konkrete Implementierung hier kann man
1
2. ANPASSUNGEN DER JVM
Programmierung hat sich etabliert. Aber manchmal
gibt es den Wunsch Funktionalitäten aus mehr als
einer Klasse in einer neuen Klasse zu vereinen.
Dynamische Sprachen zeichnen sich unter anderem
dadurch aus, dass sie ihren Programmablauf zur
Laufzeit verändern können und Schnittstellen sich
zur Laufzeit ändern können.
Dazu unterstützen manche Programmiersprachen
die Mehrfachvererbung. Diese hat jedoch einen
Nachteil: Es können Methoden über
unterschiedliche Pfade mehrfach geerbt werden
Beide Problemstellungen werden nun an einem
Beispiel erläutert. Darauf folgt eine weitere
Anforderung an die Java Virtual Machine bezüglich
Methoden.
2.1.
Klasse A
DYNAMISCHE AUFRUFE
.Klasse B
In 1.2 wurde bereits angedeutet das es sinnvoll sein
könnte in einem Abrechnungssystem die
Preisfunktion dynamisch anzupassen. Konkret
könnte man hier eine Online Shopping System
betrachten.
Klasse D
Hier erbt die Klasse D die Methoden der Klasse A
über die Klassen B und C. Es gibt also
Mehrdeutigkeiten.
Es wird immer wichtiger für Händler schnell auf
den Markt zu reagieren und somit auch eine flexible
Preispolitik zu führen. Nach dem Händler A in
seinem Shop Kampfpreise eingeführt hat, sieht sich
Händler B im Zugzwang und muss nun auch eine
Aktion anbieten. Dabei denkt er an eine „drei zum
Preis von zwei“ Aktion. Sein System unterstützt
eine derartige Rabattierung jedoch nicht. Er kann
bisher nur auf jeden Artikel einen festen Prozentsatz
gewähren.
Diese Mehrdeutigkeiten sollen mit Traits vermieden
werden.
Traits kann man sich wie Puzzle Teile vorstellen.
Mehrere Puzzle Teile ergeben ein Gesamtbild. Zum
Beispiel besteht eine Klasse, welchen einen Kreis
abbilden soll, aus einem Teil, welcher die
Eigenschaften des Kreises repräsentiert und einem
Teil, welcher die Visualisierung implementiert.
Um das System auf solche eine Aktion um zu
rüsten, müssten die Entwickler im Shopping System
die Preisfunktion anpassen. Danach muss das
System runter gefahren werden, die neue Software
eingespielt werden und dann das System erneut
gestartet werden. So fern die Aktion 1-2 Wochen
laufen soll, kann man die jeweiligen Umstellungen
in der Nacht durchführen.
Es werden also zwei Traits benötigt. Der Trait
Circle stellt fertig implementierte Methoden zur
Verfügung, mit denen man den Umfang und die
Fläche eines Kreises berechnen kann.
Der Trait CircleDrawing stellt eine draw Methode
zur Verfügung, welche die Visualisierung für eine
spezifische Technologie übernimmt.
Jetzt möchte Händler B allerdings daraus eine
Tagesaktion machen. Morgen soll etwas anderes
kommen. Genaues weiß er erst am Morgen. Seine
Kunden sollen das Gefühl haben etwas zu
verpassen, wenn Sie nicht jetzt etwas kaufen.
Traits sind Zustandslos und haben somit keine
Variablen. Beiden Traits fehlt also die Fähigkeit
den Radius speichern zu können und stellen daher
Methoden ohne Implementierung bereit, um auf
fehlende Informationen zugreifen zu können. z.B.
getRadius(). Die Methode getRadius() muss nun
beim zusammenfügen der Traits in einer Klasse
implementiert
werden.
Dabei
kann
die
Implementierung auch durch die Einbindung eines
weiteres Traits geschehen. In diesem Fall muss die
Methode jedoch durch die Klasse selbst
implementiert werden.
Neu Implementierung der Preisfunktion und
Deployment der neuen Software sind für dieses
Szenario etwas zu starr. Daher ist es hier
wünschenswert einen dynamischen Eingriffspunkt
zu haben, mit dem man zur Laufzeit die
Preisfunktion überladen kann.
2.2.
Klasse C
TRAITS
Die einfache Vererbung in der Polymorphen
Wenn unsere Kreis Klasse nun eine andere
2
Visualisierungstechnologie unterstützen soll, muss
lediglich ein anderer Trait eingebunden werden.
•
invokevirtual
Aufruf von Objektmethoden
Mit den Traits haben wir nun also ein
Baukastensystem mit dem wir das Problem der
mehrdeutigen Abhängigkeiten nicht mehr haben.
Denn Traits kennen keine Vererbung.
•
2.3.
•
Aufruf von Interface Methoden
METHODEN
Die Java Virtual Machine ist auf die Benutzung von
Klassen ausgelegt. Jedoch sind nicht alle Sprachen
objektorientiert. Aber selbst dann könnte es
erforderlich sein einzelne Methoden zur Laufzeit
nach zu laden, um Klassenmethoden zu
überschreiben.
invokestatic
Aufruf von statischen Methoden
•
invokespecial
Aufruf von Methoden, welche nicht ins
obige Konzept passen.
Zu den class Dateien sollte es also auch method
Dateien geben, welche ausschließlich von Klassen
unabhängige Methoden enthalten. Die Dateinamen
und die Verzeichnisstruktur ergeben dabei einen
Namespace in dem die Methoden organisiert sind.
2.4.
invokeinterface
3.1.
GEDÄCHTNIS FÜR INVOKE
BEFEHLE
Wenn man obige Aufrufe zur Laufzeit verändern
möchte, kann man dieses mit einem einfachen
Ansatz erreichen. Man spendiert ihnen ein
Gedächtnis. Immer dann wenn dieses leer ist, wird
das Standardverhalten, wie man es gewohnt ist,
ausgeführt. Um einen Aufruf um zu lenken muss in
diesem Gedächtnis ein Referenz zu einer Methode
hinterlegt werden.
DYNAMISCHES NACHLADEN
VON CODE
Im weiteren muss der Bytecode Loader für die
method Dateien angepasst sein und auch die
Möglichkeit bieten Code zur Laufzeit nachzuladen.
Dies kann z.B. durch eine Überwachung des
Programmverzeichnisses
geschehen.
Bei
Veränderung bzw. hinzufügen von Dateien wird der
enthaltene Code in das laufende Programm
eingefügt. Sofern vorhandener Code überschrieben
wird muss der Bytecode Loader auch eine
Versionsverwaltung führen und somit nur für neu
erstellte Objekte den neuen Code benutzen. Alte
Objekte sollten sich möglichst unverändert
verhalten.
Wenn wir mit diesem Kniff die bestehenden invoke
Befehle verändern, dann bekommen wir allerdings
wieder ein Sicherheitsproblem. Daher sollten neue
invoke Befehle eingeführt werden:
Denkbar wäre auch über eine Annotation zu
erlauben auch vorhandene Objekte dem neuen Code
entsprechend anzupassen.
•
invokedynamicvirtual
•
invokedynamicinterface
•
invokedynamicstatic
•
invokedynamicspecial
Weiterhin wäre ein Kommandozeilentool hilfreich
mit dem man eine laufende Virtual Machine
manipulieren und Byte Code nach laden kann.
Damit kann man Bytecode erzeugen, welcher nur an
bestimmen Punkten zur Laufzeit verändert werden
kann.
3. IMPLEMENTIERUNG
DYNAMISCHER AUFRUFE
Für die unabhängigen Methoden wird eigentlich
auch noch eine Möglichkeit benötigt diese explizit
aufzurufen, wenn sie nicht über die dynamische
Zuordnung oben aufgerufen werden. Daher fehlt
noch der Befehl
In der Java Virtual Machine gibt es vier invoke
Befehle, welche für die Ausführung eines Befehls
verantwortlich sind:
invokedynamicmethod
3
Jedoch
gelten
auch
hier
die
selben
Sicherheitsbedenken und es müsste daher auch der
Befehl
Gültigkeitsbereich angegeben werden.
4. IMPLEMENTIERUNG VON
TRAITS
invokemethod
folgen.
3.2.
Traits könnte man nun nativ implementieren, jedoch
aufgrund ihrer Eigenschaft nur Methoden zu
implementieren kann man auf bestehenden
Konzepten in der Java Virtual Machine aufbauen.
Eigentlich sind Traits Interfaces, welche teilweise
Implementierungen für ihre Methoden bereitstellen.
MANIPULATION ZUR LAUFZEIT
Referenzen auf neue Methoden werden in den
meisten Fällen nicht auf im ursprünglichen Code
vorhandene Methoden gesetzt.
Für einen Trait benötigt man die Möglichkeit
unabhängige Methoden mit einem Interface in
Verbindung zu bringen. Es handelt sich hier um
Interface Aufrufe die auf Methoden umgelenkt
werden
sollen.
Daher
kommt
hier
invokedynamicinterface,
statt
dem
alten
invokeinterface in Frage. Der Zeiger wird zur
Laufzeit gesetzt, also genau dann wenn die
dazugehörigen Method Dateien geladen werden und
über die Kontext Information mit dem Interface
verbunden werden.
Wenn
einzelne
Methoden
einer
Klasse
überschrieben werden sollen, sollte der Namespace
der nach zu ladenen Methode den Packagepfad und
Klassennamen enthalten. Dann gäbe es einen klaren
Bezug zwischen der Methode und der
Klassen-/Objektmethode. Die neue Referenz kann
dann in das Gedächtnis des invoke Befehls
geschrieben werden.
Dabei ist nun zu unterscheiden, ob nun jeder
dynamische Aufruf eine eigene Referenz haben
kann oder ob es für den Methodenaufruf an sich
eine zentrale Referenz im Speicher geben soll.
Diese Betrachung ist an dieser Stelle bezüglich der
Performance wichtig. Beide Lösungen haben
Vorteile. Eine zentrale Referenz ist einfacher zu
pflegen. Verteilte Referenzen müssen erst gefunden
werden. Jedoch macht es dann keinen Sinn alle
Referenzen auf den selben Wert zu setzen. Es
könnte also nützlich sein den Kontext der neuen
Methode genauer zu definieren. Und zwar nicht nur
darüber, welche Methode überladen werden soll,
sondern auch an welcher Stelle im Programm. Der
Implementierung der neuen Methode muss also ein
Gültigkeitsbereich mitgegeben werden.
Möchte man nun die Traits für die Verwendung
zusammensetzen muss entweder eine Komposition
zum Zeitpunkt der Übersetzung des Programmes
definiert werden oder man muss diese Komposition
zur Laufzeit zusammen setzen. Dafür muss es
möglich sein Interfaces zur Laufzeit zu einer Klasse
hinzuzufügen.
Der Vorteil ein Interface zur Laufzeit in ein Objekt
einzufügen besteht darin, dass man keine leere
Implementierung vorhalten muss.
Möchte man z.B. dynamisch zur Laufzeit eine
geeignete Visualisierung für ein geometrisches
Objekt auswählen, müsste mit der statischen
Methode zur Übersetzungszeit das Trait Interface
mit einer leeren Methode implementieren. Das kann
dazu führen, dass diese leere Methode tatsächlich
produktiv genutzt wird. Fügen wir das betroffene
Interface erst bei der Verlinkung mit dem
gewünschten Trait durch, ist eine Verwendung der
leeren dummy Methode ausgeschlossen.
Somit eröffnen sich drei verschiedene Wege eine
neue
Methode
in
den
Programmablauf
einzuschleusen:
1. Der Byte Code Loader findet eine neue
method Datei und setzt entsprechend dem
Namespace alle Referenzen.
2. Der Byte Code Loader findet in der method
Datei einen Gültigkeitsbereich und setzt
diesem entsprechend die Referenzen.
Problematisch dabei sind dann allerdings noch die
Typprüfungen, wenn man dann eine Methode der
Kompositionsklasse aufruft, welche aber erst durch
einen Trait bereit gestellt wird.
3. Die method Datei wird über ein
Kommandozeilen Tool in die JVM geladen
und mit diesem kann auch ein
Es ergeben sich somit Situationen in denen
Ausnahmefehler auftreten können. Das aufrufen
4
einer leeren Referenz könnte jedoch vom System
abgefangen werden, in dem geprüft wird, ob mit
dem Interface auch dazu gehörige Methoden
geladen werden bzw. ob passende Methoden in der
Klasse vorhanden sind.
5. FAZIT
Wenn man den invoke Methoden Block um
dynamische Aufrufe ergänzt lassen sich bereits
einige dynamische Konzepte umsetzen.
Die Art und Weise mit der eine Methode zur
Laufzeit überladen wird, sollte recht bedacht
gewählt werden. Wenn man es zulässt nur einzelne
Aufrufe an einer ganz bestimmten Stelle im
Programm zu ersetzen, kann dabei die
Übersichtlichkeit verloren gehen. Es könnte unklar
sein, welche konkrete Funktion das Programm zu
einem bestimmten Zeitpunkt eigentlich erfüllt.
Insgesamt erhält man mit obigen Ansätzen kein
völlig neues System, aber einige sinnvolle
Erweiterungen eines etablierten und getesteten
Systems.
Quellen
1. http://openjdk.java.net/projects/mlvm/subpr
ojects.html
2. http://blogs.sun.com/jrose/entry/notes_on_a
n_architecture_for
3. http://www.parrot.org/
4. http://www.dalvikvm.com/
5. JSR 292: http://jcp.org/en/jsr/detail?id=292
5
Herunterladen