Laufzeitumgebungen am Beispiel der Java Virtual Machine

Transcription

Laufzeitumgebungen am Beispiel der Java Virtual Machine
Westfälische Wilhelms-Universität Münster
Ausarbeitung
Laufzeitumgebungen am Beispiel der Java Virtual
Machine
im Rahmen des Seminars Übersetzung von künstlichen Sprachen
Benjamin Leenen
Themensteller: Prof. Dr. Herbert Kuchen
Betreuer: Tim Majchrzak
Institut für Wirtschaftsinformatik
Praktische Informatik in der Wirtschaft
Inhaltsverzeichnis
1 Einleitung
1
2 Übersetzung und Ausführung von Programmen
2
2.1
Compiler, Interpreter und Laufzeitumgebungen . . . . . . . . . . . .
2
2.2
Virtuelle Maschinen als Laufzeitumgebung . . . . . . . . . . . . . . .
3
3 Die Java Virtual Machine
4
3.1
Die Programmiersprache Java . . . . . . . . . . . . . . . . . . . . . .
4
3.2
Das class-Dateiformat . . . . . . . . . . . . . . . . . . . . . . . . . .
5
3.3
Speicherbereiche zur Laufzeit . . . . . . . . . . . . . . . . . . . . . .
6
3.4
Verifizierung von class-Dateien . . . . . . . . . . . . . . . . . . . . .
8
3.5
Die Ausführung der JVM– Laden, Binden und Initialisieren . . . . . . 10
3.6
Sicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.7
3.6.1
Die Architektur . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.6.2
Risiken und Herausforderungen . . . . . . . . . . . . . . . . . 14
Die Squawk Java Virtual Machine . . . . . . . . . . . . . . . . . . . . 16
3.7.1
Die geteilte Architektur . . . . . . . . . . . . . . . . . . . . . 16
3.7.2
Das suite-Dateiformat . . . . . . . . . . . . . . . . . . . . . . 18
3.7.3
Die geräteseitige VM . . . . . . . . . . . . . . . . . . . . . . . 19
4 Fazit und Ausblick
21
A Übersetzung von Java-Code in Bytecode
22
Literaturverzeichnis
23
i
Kapitel 1: Einleitung
1
Einleitung
In den Anfangszeiten der Programmierung wurden Computer über Maschinensprachen gesteuert, die über Nullen und Einsen den Rechnern direkt die Anweisungen
vorgegeben haben [ALUS08, S. 16]. Um eine intuitivere und weniger fehleranfällige
Programmierweise zu ermöglichen, entstanden mit der Zeit zunächst Assemblersprachen zur einfacheren Darstellung von Maschinenbefehlen und im Weiteren höhere
Programmiersprachen wie C, Cobol, C# und Java, in welchen Programme in intuitiveren abstrakten Notationen verfasst werden.
Zur Ausführung von Programmen, die nicht in Maschinensprache geschrieben worden sind, muss die Quellsprache jener in diese übersetzt werden [ALUS08, S. 3].
Dies ist die Aufgabe von Compilern. Eine der wichtigsten Aufgaben dieser, ist die
Erkennung von Fehlern im Quellcode während der Übersetzung. Zudem erzeugt und
verwaltet er eine Laufzeitumgebung, in welcher die Zielprogramme als Ergebnis des
Kompilierens ausgeführt werden können [ALUS08, S. 520].
Eine andere Möglichkeit bietet der Einsatz einer virtuellen Maschine (VM) als Laufzeitumgebung. Dabei wird der Quellcode nicht direkt in Maschinencode übersetzt,
sondern in einen Zwischencode, welcher von der VM bei der Ausführung verarbeitet
wird [Re06, S. 766]. Dies bringt den Vorteil mit sich, dass der Zwischencode unabhängig von Hardware und Betriebssystem des Zielrechners erstellt werden kann.
Lediglich die Implementierung einer VM muss auf diesen abgestimmt sein. Dies
ermöglicht die Übertragung eines Programms auf jeden anderen Rechner, auf welchem sich dieses dann immer gleich verhält [LY99, Kap. 1].
Diese Ausarbeitung gibt in Kapitel 2 zunächst einen Überblick über die einzelnen
zu beachtenden Komponenten, die bei der Ausführung von Programmen höherer
Programmiersprachen von Bedeutung sind, wie etwa Compiler, Laufzeitumgebungen
und eine VM in der Funktion von Laufzeitumgebungen. Kapitel 3 widmet sich im Detail der Java Virtual Machine (JVM), welche als Laufzeitumgebung für Java fungiert.
Neben deren grundlegendem Aufbau und der internen Verarbeitung von übersetztem
Java-Code, beschreibt 3.6 wichtige Aspekte der Java-Sicherheitsarchitektur und 3.7
zum Abschluss die Squawk JVM als Beispiel für eine Implementierungsmöglichkeit
für kleine tragbare Geräte.
1
Kapitel 2: Übersetzung und Ausführung von Programmen
2
2.1
Übersetzung und Ausführung von Programmen
Compiler, Interpreter und Laufzeitumgebungen
Die Übersetzung von Quellprogrammen in Zielprogramme wird im Allgemeinen
durch einen Compiler durchgeführt (Vgl. Abb. 1) [ALUS08, S. 3]. Soll ein Quellprogramm, welches in einer höheren Sprache verfasst ist, vom späteren Benutzer
ausgeführt werden, um es bspw. für Ein- und Ausgabevorgänge nutzen zu können,
muss das zugehörige Zielprogramm in ausführbarer Maschinensprache vorhanden
sein.
Abbildung 1: Verfahrensweise eines Compilers, in Anlehnung an [ALUS08, S. 3]
Eine andere Möglichkeit zur Ausführung eines Programms bietet ein Interpreter
[ALUS08, S. 3]. Im Gegensatz zum Compiler übersetzt ein Interpreter nicht vor der
Ausführung, ein Quell- in ein Zielprogramm, sondern interpretiert jenes direkt zur
Laufzeit. So z. B. bei einer Nutzereingabe (Vgl. Abb. 2). Dieses Vorgehen hat in der
Regel eine bessere Erkennung von Fehlern als Vorteil gegenüber der Verwendung
von Compilern, da der Interpreter die Befehle zur Laufzeit sequentiell abarbeitet.
Der Einsatz von Compilern beschleunigt dafür jedoch die Verarbeitung des Zielprogramms zur Laufzeit, da die Maschinensprache direkt auf dem Zielrechner ausgeführt
werden kann.
Abbildung 2: Verfahrensweise eines Interpreters, in Anlehnung an [ALUS08, S. 3]
Der komplette Zeitraum, in dem die Ausführung eines kompilierten Zielprogramms
stattfindet, ist die Laufzeit [DN07, S. 27]. Jene findet in einer Laufzeitumgebung
statt, die der Compiler gemeinsam mit Zielrechner und Betriebssystem verwaltet
[ALUS08, S. 520]. Diese Umgebung sorgt dafür, dass die kompilierten Abstraktionen des Quellprogramms auf dem Zielrechner unterstützt werden. Hierzu sorgt sie
z. B. für die Zuweisung von Speicher für Objekte, die Verknüpfung zwischen Variablen und Methoden und die Bereitstellung von Schnittstellen zum Betriebssystem
und anderen Programmen.
2
Kapitel 2: Übersetzung und Ausführung von Programmen
2.2
Virtuelle Maschinen als Laufzeitumgebung
Bei einigen höhreren Programmiersprachen, wie etwa Java und C#, fungiert eine VM
als Laufzeitumgebung [Re06, S. 766]. Eine VM stellt eine Umsetzung einer Rechnerarchitektur als Software dar und verfügt dabei über einen eigenen Befehlssatz
und Speichersegmente für die auszuführenden Programme. Bei einer VM werden
die Startprogramme im ersten Schritt in Zwischenprogramme übersetzt. Diese bestehen nicht aus ausführbarer Maschinensprache, sondern sind so konzipiert, dass die
VM sie ausführen kann. Meistens, so z. B. auch in Java, kommt dafür Bytecode zum
Einsatz. Diese Zwischensprache ist unabhängig von dem auszuführenden Zielrechner. Stattdessen repräsentiert sie den Befehlssatz einer VM bzw. die VM-Sprache
[Ke01, S. 12 f.] (Vgl. Tab. 1 in Anhang A). Diese muss schließlich in der Lage sein,
diesen Code verarbeiten und für die Ausführung des zugehörigen Programms auf
dem Zielrechner sorgen zu können.
Nach der Erstellung des Bytecodes durch einen Compiler muss jener durch die VM
Abbildung 3: Verfahrensweise eines Hybridcompilern,
in Anlehnung an [ALUS08, S. 4]
bearbeitet werden [Re06, S. 766]. Eine Option dafür ist die Interpretierung des Zwischencodes durch einen Interpreter in der VM (Vgl. Abb. 3). Andere Möglichkeiten
sind die Übersetzung des Bytecodes in Maschinensprache beim Laden dessen in
die VM, wodurch der Interpreter überflüssig werden würde, oder die Verwendung
eines Just-in-time-Compilers (JIT-Compiler). Dieser übersetzt zur Laufzeit die am
häufigsten verwendeten Programmteile in Maschinensprache, wodurch deren Ausführung beschleunigt wird. Die restlichen Programmteile werden weiterhin interpretiert.
3
Kapitel 3: Die Java Virtual Machine
3
Die Java Virtual Machine
3.1
Die Programmiersprache Java
Die Motivation zur Entwicklung der Programmiersprache Java waren die Gewährung
einer generellen - und insbesondere sicheren Ausführbarkeit von Java-Anwendungen
auf unterschiedlichen Rechnern und eines sicheren Transports dieser Anwendungen
über Netzwerke [LY99, Kap. 1.1].
Java ist eine objektorientierte Programmiersprache, welche Programmierern eine
möglichst schnelle und einfache Beherrschung derselbigen ermöglichen soll [GJSB05,
Kap. 1]. Hierzu abstrahiert Java von den Instruktionen, welche bei der späteren
Ausführung durch die VM abgearbeitet werden. So erfolgt bspw. die Speicherverwaltung automatisch, um Speicher nicht wie anderen Sprachen manuell freigeben zu
müssen.
Ein Java-Programm besteht aus einzelnen Klassen, welche zu Paketen zusammengefasst werden können [LY99, Kap. 2]. Klassen können eigene Klassenvariablen,
Methoden und Konstruktoren enthalten. Die beiden Datentypen, die Variablen und
Ausdrücken zugewiesen werden können, sind primitive - und Referenzdatentypen.
Java ist dabei strikt getypt. Alle Datentypen von Variablen und Ausdrücken sind
somit zum Zeitpunkt des Compilierens bekannt. Dadurch können dabei leichter Fehler erkannt werden. Primitive Datentypen sind boolean und numerische Werte, wie
int und char. Referenztypen sind Klassen- , Interface- und Array-Typen. Bei Klassen existieren die beiden vordefinierten Typen Object, welche die Elternklasse aller
anderen Klasse ist, und String, deren Instanzen Zeichenketten darstellen.
Bezeichner, z. B. für Variablen, Kommentare und Zeichenketten in Form von
char und String bestehen alle aus einer Folge von Buchstaben und Zahlen in Form
der Zeichenkodierung Unicode 1 . Der restliche Java-Code besteht aus American Standard Code for Information Interchange (ASCII )-Zeichen. Unicode kommt in den
genannten Fällen zum Einsatz, da es die meisten existenten natürlichen Sprachen
darstellen kann, wodurch Entwickler auf der ganzen Welt z. B. Bezeichner in ihrer
eigenen Sprache darstellen können.
Die Behandlung von Ausnahmen oder unerwünschten Vorgängen findet über das
Werfen einer exception statt. Dies kann automatisch geschehen, über den Befehl
throw aber auch manuell geschehen. In jedem Fall können diese Ausnahmen im
Programmcode abgefangen werden, um im Falle einer solchen darauf reagieren zu
können.
1
http://www.unicode.org
4
Kapitel 3: Die Java Virtual Machine
Die vom Programmierer erstellten java-Dateien, welche den Programmcode beinhalten, werden vom Compiler in class-Dateien übersetzt, die u. a. VM-Befehle in
Form von Bytecode enthalten. Die JVM kann nur das class-Dateiformat lesen und
keinen ursprünglichen Java-Code. Somit besteht prinzipiell die Möglichkeit auch andere Programmiersprachen auf einer JVM ausführen zu lassen, wenn der Startcode
in ein gültiges class-Dateiformat überführt werden kann.
Die Spezifikation der JVM von Lindholm, Yellin bechreibt eine mögliche Implementierung einer VM [LY99, Kap. 3]. Diese kann jedoch auch anders umgesetzt
werden. Die einzige Anforderung dafür ist, dass später das class-Dateiformat und
die darin definierten Funktionen korrekt ausgelesen und ausgeführt werden. Einige
Implementierungsdetails werden somit bewusst dem Entwickler der VM überlassen,
so beispielsweise die Wahl des Algorithmus zur Garbage Collection. Eine Umsetzung dieser Spezifikation stellt die Java HotSpot VM dar, welche als Kern der Java
Laufzeitumgebung (Java Runtime Environment) von Sun Microsystems die gängige
JVM darstellt [SM09a].
3.2
Das class-Dateiformat
Eine class-Datei enthält immer die Bytecode-Darstellung einer Klasse oder Interface und enthält einen Datenstrom, der aus 8-bit Bytes besteht [LY99, Kap. 4].
Diese können auch zu Werten, die aus mehreren Bytes bestehen, zusammengesetzt
werden. Eine korrekte class-Datei enthält immer eine einzige ClassFile Struktur
(Notation ähnlich der des Datentyps Struct der Programmiersprachen C und C++):
ClassFile {
u4 magic;
u2 minor version;
u2 major version;
u2 constant pool count;
cp info constant pool[constant pool count-1];
u2 access flags;
u2 this class;
u2 super class;
u2 interfaces count;
u2 interfaces[interfaces count];
u2 fields count;
field info fields[fields count];
u2 methods count;
5
Kapitel 3: Die Java Virtual Machine
method info methods[methods count];
u2 attributes count;
attribute info attributes[attributes count];
}
(u1, u2 bzw. u4 stehen dabei für die Anzahl von Bytes (1 -, 2 - bzw. 4 Bytes)),
aus welchen ein Wert besteht)
Die class-Datei beginnt immer mit dem Element magic, welches diese über den
festen Wert 0xCAFEBABE als solche identifiziert [LY99, Kap. 4.2].
Die Festlegung von Zugriffsrechten über access flags existiert nicht nur für
ganze Klassen, sondern ebenfalls für einzelne Felder und Methoden. Hierüber kann
festgelegt werden, wer direkt auf diese zugreifen darf. So sind Felder und Methoden
bspw. über das Angeben von public von überall aus sichtbar und über private
hingegen nur für Klassen aus dem selben Paket.
Der Eintrag fields[] umfasst alle Klassenvariablen inkl. zusätzlicher Informationen wie bspw. den Typ des Feldes (String, int, boolean usw.) die Zugriffsrechte
(public, private usw.) und zugehörige Attribute (z. B. Annotations) [LY99, Kap.
4.5-4.8]. Der Aufbau von methods[] entspricht im Prinzip diesem. Als Typ wird
der jeweilige Rückgabewert angegeben. Als Attribute werden dabei u. a. der auszuführende Code in Maschinensprache (im Element code[]) und die Exceptions,
welche dabei geworfen werden können, geführt.
Der Konstantenpool (constant pool count[]) beinhaltet Referenzen auf andere Klassen, Felder und Methoden. Diese Referenzen bestehen aus allen UnicodeBezeichnern, die für eine eindeutige Identifikation notwendig sind. So werden Methoden bspw. über die jeweilige Klasse, den Bezeichner der Methode, den Rückgabewert
und alle Parameter eindeutig identifiziert. Um sich wiederholte Angaben dieser und
somit Speicherplatz sparen zu können, werden diese Informationen nur einmal pro
class-Datei in dem Konstantenpool angeben. Alle Elemente der class-Datei, die
einen Verweis auf ein anderes Element benötigen, nutzen deshalb eine symbolische
Referenz auf den passenden Eintrag im Konstantenpool.
3.3
Speicherbereiche zur Laufzeit
Die JVM umfasst mehrere Speicherbereiche, die zur Laufzeit von Bedeutung sind
[LY99, Kap. 3.5]. Diese unterteilen sich in solche, die vom Start bis zum Ende der
JVM existieren, und solche, die einem Thread zugewiesen sind und von dessen Dauer
abhängig sind. Ein Thread beinhaltet die Abarbeitungsabfolge mehrerer Instruktio-
6
Kapitel 3: Die Java Virtual Machine
nen nacheinander, wobei die JVM die gleichzeitige Ausführung mehrerer Threads unterstützt (Weiteres zu Threads in [LY99, Kap. 2.19]). Die Ausführung eines Threads
ist dabei immer unabhängig von der anderer Threads, auch wenn sich diese Werte
und Objekte von einem gemeinsam genutzten Speicherbereich teilen. Allerdings besitzt jeder Thread ein eigenes Register, das PC (Program Counter) Register [LY99,
Kap. 3.5]. Dieses enthält die Adresse der aktuell auszuführenden JVM-Instruktion,
wenn diese nicht Bestandteil einer nativen Methoden ist, also einer Methode, die
in einer plattformabhängigen Sprache, gewöhnlich C, C++ oder eine Assemblersprache, geschrieben ist (Weiteres zu nativen Methoden in [GJSB05, Kap. 8.4.3.4]).
Bei diesen ist der Registereintrag undefiniert. Zum zweiten ist jedem Thread ein
eigener Stack (der Java Virtual Machine Stack ) zugeordnet, welcher gleichzeitig mit
dem Thread erstellt wird. Dieser Stack speichert lokale Variablen und Zwischenergebnisse in Form von Frames. Unterstützt die JVM die Ausführung von nativem
Code, umfasst diese zusätzliche Stacks zur Ausführung dessen, die Nativen Methoden Stacks, deren Bestehen normalerweise von der Dauer eines Threads abhängig ist.
Ein Frame wird beim Aufruf einer Methode als Teil des Stacks des aktuellen Threads
erstellt und mit der Fertigstellung dieser Methode wieder gelöscht [LY99, Kap. 3.6].
Ein Frame besteht aus einem eigenen Array, welcher lokale Variablen enthält, dem
Variablenarray, einem eigenen Operandenstack, welcher nach dem Last-In-FirstOut (LIFO) System zur Ablage und weiteren Verarbeitung von Konstanten und
Variablen genutzt wird, und einem Verweis auf den Laufzeit-Konstantenpool. Dieser wird zusammen mit der jeweiligen Klasse erstellt und stellt eine Laufzeitrepräsentation des Konstantenpools aus der class-Datei dieser Klasse dar.
Die Speicherbereiche, welche beim Start der JVM erzeugt werden und sich alle
Threads teilen, sind der Heap und der Methodenbereich (Method Area) [LY99, Kap.
3.5]. Der Heap stellt für alle Klasseninstanzen und Arrays Speicher zur Verfügung.
Dieser wird von einem automatischen Speicherverwaltungssystem verwaltet, dem
Garbage Collector. Dessen Implementierung bleibt dabei dem Entwickler der VM
überlassen. Der Methodenbereich umfasst klassenabhängige Daten, wie etwa den
Laufzeit-Konstantenpool, Felder- und Methodendaten und den Code der Methoden
und Konstruktoren.
7
Kapitel 3: Die Java Virtual Machine
3.4
Verifizierung von class-Dateien
Vor dem eigentlichen Laden einer class-Datei muss überprüft werden, ob deren
Format dem vorgegebenen class-Dateiformat entspricht [LY99, Kap. 4.9]. Dabei
wird kontrolliert, ob die ersten vier Bytes den korrekten magic-Eintrag enthalten,
alle vorhandenen Attribute von korrekter Länge sind und die Datei weder kürzer
noch länger ist als vorgeschrieben.
Vor der Verarbeitung einer class-Datei durch die JVM muss eine der bedeutendsten Schritte dieser Verarbeitung stattfinden, die Verifizierung. Diese prüft, ob eine
class-Datei alle notwendigen Bedingungen, welche aus statischen und strukturellen
bestehen, erfüllt [LY99, Kap. 4.11]. Dadurch kann sichergestellt werden, dass zur
Laufzeit bspw. kein Überlauf (Overflow) und Unterlauf (Underflow) auf dem Operandenstack möglich ist und alle Parameter der verwendeten JVM-Instruktionen
einen zulässigen Datentyp haben.
Ein Java-Compiler muss im Normalfall die Einhaltung aller statischen Bedingungen überprüfen. Die JVM kann sich beim Ladevorgang jedoch nicht sicher sein,
dass der Compiler, der die class-Datei erstellt hat, dies tatsächlich beachtet hat
und ob diese somit im korrekten Format vorliegt. Dies ist besonders bei der Verteilung von class-Dateien über das Internet von Bedeutung. Die Verifizierung, ob alle
notwendigen Bedingungen erfüllt sind, folgt zum Zeitpunkt des Bindens (Vgl. Kap.
3.5). Dies verbessert die Schnelligkeit bei der Ausführung durch den Interpreter.
Statische Bedingungen sind zum einen für die Attribute der Klasse selbst (Vgl.
Kap. 4.8 in [LY99]) einzuhalten und zum anderen für den ausführbaren Code aller
Methoden einer Klasse, für welchen zudem noch strukturelle Bedingungen existieren
[LY99, Kap. 4.10]. Statische Bedingungen definieren im Allgemeinen die Wohlgeformtheit der Datei und in diesem speziellen Fall, wie die JVM-Instruktionen angegeben werden müssen. So wird bspw. vorgegeben, dass das Element code[] mindestens die Länge von eins haben muss und welche Operanden für welche Instruktionen
verwendet werden dürfen. Strukturelle Bedingungen geben an, wie sich die Instruktionen in gegenseitiger Beziehung zueinander verhalten müssen bzw. dürfen.
Die Verifizierung des Codes aller Methoden einer class-Datei wird durch den
Bytecode Verifier durchgeführt [LY99, Kap. 4.11.1.2]. Dieser teilt im ersten Schritt
alle Bytes, die den Code repräsentieren, in die einzelnen Instruktionen auf und analysiert diese, um bspw. deren mögliche Operanden erkennen und auf ihre Zulässigkeit
überprüfen zu können. Dies geschieht für jede Methode unabhängig von den anderen. Für jede Instruktion erfasst der Bytecode Verifier vor deren Ausführung den
8
Kapitel 3: Die Java Virtual Machine
Inhalt des Operandenstacks, inkl. der Größe und des Typs aller Einträge, und des
Variablenarrays, inkl. des Typs aller Variablen.
Als nächstes wird das Datenflussanalyseprogramm (data-flow analyzer) gestartet,
welches die vorbereiteten Instruktionen durchgeht und so die Zuweisung von Werten
und Speicherplätzen an Variablen simulieren kann. Zur Kontrolle, ob eine Instruktion noch überprüft werden muss, wird für eine jede ein geändert-Bit ( changed“ bit)
”
geführt. Beim Start der Analyse ist der Operandenstack noch leer, allen lokalen Variablen, welche Parameter der jeweiligen Methode darstellen, werden initiale Werte
zugewiesen und das geändert-Bit der ersten Instruktion wird auf 1 gesetzt.
Das Analyseprogramm durchläuft alle Instruktionen schleifenförmig, bis jedes
geändert-Bit den Wert 0 hat. Zunächst sucht jenes nach der ersten Instruktion,
dessen geändert-Bit auf 1 steht und setzt dieses auf 0.
Im zweiten Schritt werden die Auswirkungen der Instruktion auf den Operandenstack und den Variablenarray simuliert. Liest die Instruktion Werte des Operandenstacks, wird untersucht, ob dieser genügend Werte enthält und ob diese dem
notwendigen Typ entsprechen. Ist ebenfalls ein schreibender Zugriff auf den Operandenstack vorgesehen, muss geprüft werden, ob genügend Platz hierfür vorhanden
ist. In diesem Fall wird der Schreibvorgang durchgeführt. Bei einem lesenden Zugriff
auf lokale Variablen, werden die referenzierten Variablen ebenfalls auf ihren Typ hin
getestet. Soll dieser Typ von einer Instruktion verändert werden, wird dies vorgenommen. Schlägt eine der Überprüfungen der beiden lesenden Vorgänge fehl, tut
dies auch die gesamte Verifizierung.
Als drittes werden alle möglichen Folgeinstruktionen bestimmt. Diese können
bspw. die folgende in der Liste aller Instruktionen sein, aber auch eine über den
Befehl goto oder eine Verzweigung erreichbare sein.
Als letztes werden die aktuellen Operandenstack und Variablenarray mit den
beiden aller Folgeinstruktionen zusammengeführt. Wird eine dieser zum ersten mal
untersucht, werden bisheriger Stack und Array als der Status vor der Ausführung
der Folgeinstruktion festgehalten. Wurde diese bereits einmal überprüft, müssen die
beiden jeweiligen Stacks und Arrays zusammengeführt werden. Dabei ist zu beachten, dass bei den Stacks sowohl die Anzahl aller Werte als auch deren Typen
übereinstimmen müssen. Bei den Arrays werden die Werte paarweise auf ihren jeweiligen Typ hin überprüft. Wird eine dieser Bedingungen nicht erfüllt, scheitert die
komplette Verifizierung. Nach diesem vierten Schritt springt das Datenflussanalyseprogramm wieder zum ersten.
9
Kapitel 3: Die Java Virtual Machine
3.5
Die Ausführung der JVM– Laden, Binden und Initialisieren
Binden
Laden
Verifizierung
Vorbereitung
Auflösung
Initialisieren
Abbildung 4: Vorbereitung der Ausführung durch Laden, Binden und Initialisieren,
in Anlehnung an [Ve00, Kap. 7]
Klassen und Interfaces durchlaufen vor der endgültigen Ausführung drei Phasen
– Laden, Binden und Initialisieren (Loading, Linking and Initializing) (Vgl. Abb.
4). Die Verarbeitung von Java Bytecode auf der JVM beginnt mit dem Erstellen
einer initialen Klasse, welche zuvor spezifiziert werden muss, und dem Aufruf deren
main-Methode (void main(string[])) [LY99, Kap. 2.17.1, 5.2 und 5.3]. Enthält
die JVM diese Klasse noch nicht im Binärformat, wird der Prozess des Ladens,
Bindens und Initialisierens angestoßen. Die Erstellung einer Klasse erfolgt über das
Laden derselbigen.
Der Ladevorgang wird immer von einer anderen Klasse ausgelöst und erfolgt durch
einen Klassenlader (Class Loader), welcher nach einer Binärdarstellung der Klasse,
die im Regelfall durch eine class-Datei gegeben ist, sucht [LY99, Kap. 5.3]. Ein
Klassenlader kann eine Klasse direkt selbst laden und erstellen oder diese Aufgaben an einen anderen Klassenlader übertragen. Zur Laufzeit wird eine Klasse nicht
nur über ihren Namen, sondern auch über ihren Klassenlader, der sie erstellt hat,
definiert. Array-Klassen bilden hierbei eine Ausnahme, da sie nicht von einem Klassenlader, sondern von der JVM direkt erstellt werden.
Es existieren zwei verschieden Arten von Klassenladern – nutzerdefinierte - und
der Bootstrap Klassenlader [Ve00, Kap. 1]. Der Bootstrap Klassenlader ist direkter
Bestandteil der JVM. Nutzerdefinierte hingegen werden wie alle sonstigen Objekte
eines Java-Programms in Java geschrieben, in das class-Dateiformat übersetzt und
zur Laufzeit eingebunden. Dies ermöglicht die dynamische Erweiterung einer JavaApplikation zur Laufzeit, indem dadurch zu jeder Zeit neue Klassen mit zugehörigem
Klassenlader nachgeladen werden können. So bspw. über das Internet.
Eine Klasse wird immer vom selben Klassenlader erstellt wie die Klasse, welche
den Ladevorgang ausgelöst hat [LY99, Kap. 5.3]. Vor dem Ladevorgang einer Klasse
10
Kapitel 3: Die Java Virtual Machine
K mit Namen N durch den Klassenlader L prüft die JVM zunächst, ob K mit Namen
N bereits geladen worden ist. In diesem Fall ist ein erneutes Laden und Erstellen
nicht notwendig. Andernfalls stößt die JVM den Ladevorgang durch L an, wobei L
diesen auch an einen anderen Klassenlader übertragen kann. Dieser Vorgang verläuft
sowohl beim Bootstrap - als auch bei nutzerdefinierten Klassenladern im Prinzip
gleich. Nur im ersten Schritt unterscheidet sich das Vorgehen. Bei Verwendung des
Bootstrap Klassenladers sucht die JVM nach einer bereits vorhandenen, vorgegeben
Darstellung der zu ladenden Klasse K mit Namen N. Nutzerdefinierte Klassenlader
hingegen erzeugen selbst eine Darstellung in Byteform von K mit Namen N. Ist dies
erfolgreich versucht die JVM in beiden Fällen mittels dieser Darstellung und des jeweiligen Klassenladers eine Klasse mit Namen N abzuleiten. Hierbei wird zunächst
überprüft, ob die vorgeschlagene Darstellung in einer gültigen Form vorliegt. So
muss sie z. B. dem class-Dateiformat entsprechen. Zudem werden die Verweise auf
mögliche Elternklassen aufgelöst und auf ihre Korrektheit untersucht. Laufen diese
Schritte ohne Fehler ab, hält die JVM fest, dass der verwendete Klassenlader L der
erstellende der Klasse K ist.
Nach Abschluss des Ladevorgangs beginnt das Binden der Klasse K, welches die
Verifizierung und Vorbereitung von K inkl. möglicher Elternklassen und Elementtypen (bei Array-Klassen) und als letzten Schritt die optionale Auflösung umfasst
[LY99, Kap. 5.4].
Die Verifizierung umfasst die Prüfung, ob die Binärdarstellung von K zulässig
ist, also dem class-Dateiformat genügt (Vgl. Kap. 3.4). Dies kann das Laden
zusätzlicher Klassen zur Folge haben. Während des daran anschließenden Vorbereitungsschrittes weist die JVM allen Variablen von K einen Speicherort zu und
besetzt diese mit initialen Werten. Die Auflösung ist der letzte Schritt des Bindens,
während dessen die symbolischen Verweise von K auf andere Klassen durch konkrete
Werte ersetzt werden. Dieser Schritt ist an dieser Stelle optional, da die Auflösung
der symbolischen Verweise auch erst zu dem Zeitpunkt der tatsächlichen Verwendung stattfinden kann. So wird bei dem Verweis auf Felder oder Methoden einer
anderen Klasse diese zunächst über einen Klassenlader geladen, um darauf zugreifen zu können. Dabei wird bei Feldern und Methoden überprüft, ob die aufrufende
Klasse die Berechtigung dazu hat. Dies ist bspw. bei einer Deklaration als public
der Fall oder wenn beide Klassen Bestandteil des selben Pakets sind.
Die dritte und letzte zu durchlaufende Phase ist die Initialisierung einer Klasse,
in welcher den Klassenvariablen die initialen Werte zugewiesen werden, die der Ent11
Kapitel 3: Die Java Virtual Machine
wickler im Programmcode vorgesehen hat [LY99, Kap. 2.17.4 und 2.17.5]. Dabei hat
die JVM für die Synchronisation, wenn mehrere Threads gleichzeitig die Initialisierung vornehmen wollen, und rekursive Initialisierung, die bspw. auftritt, wenn eine
Klasse A die Methode einer Klasse B und B wiederum eine Methode von A aufruft,
zu sorgen.
3.6
3.6.1
Sicherheit
Die Architektur
Die Sicherheitsarchitektur von Java besteht aus den drei Hauptkomponenten Bytecode-Verifier (Vgl. Kap. 3.4), Klassenlader (Vgl. Kap. 3.5) und Sicherheitsmanager
(Security Manager) [Ec08, S. 600-603]. Dieser ist die zentrale Komponente dieser
Sicherheitsarchitektur und setzt das Sicherheitsmodell um, indem er alle Zugriffe
auf sicherheitskritische Systemressourcen zur Laufzeit kontrolliert. Dazu gehören
Zugriffe auf Dateien, Netzressourcen, Betriebssystemdienste und die Definition neuer
Klassenlader.
lokaler oder entfernter Code
geladene Klassen
JVM
A.class
B.class
C.class
Sicherheitsstrategie
Systemdomäne
Sandboxdomäne
Domäne 1
Domäne 2
freier Zugriff
Policy
Policy
Sicherheitsmanager
Systemressourcen (Daten, Speicher usw.)
Abbildung 5: Die Sicherheitsarchitektur der JVM [Ec08, S. 608]
Die allererste Version des Sicherheitsmodells ist als Sandbox-Modell bekannt
[Ec08, S. 604-606]. In diesem darf lokal erzeugter Code auf alle benötigten Systemressourcen zugreifen und Code, welcher von einem anderen Rechner stammt,
wird in einer Sandbox ausgeführt, welche diesem Code nur eingeschränkte Rechte
gibt. In einer zweiten Version wurde dieses Modell um die Möglichkeit erweitert, Code zu signieren, welcher auf einem anderen Rechner dadurch als vertrauenswürdig
eingestuft und mit allen Zugriffsrechten ausgeführt werden kann. In der aktuellen
Version ist das Sicherheitskonzept wesentlich umfangreicher (Vgl. Abb. 5), um einer
12
Kapitel 3: Die Java Virtual Machine
Java-Anwendung nur noch so viele Rechte geben zu können, wie benötigt. So fällt
auch die grundsätzliche Unterscheidung zwischen lokalem und entferntem Code weg.
Die generelle Sicherheitsstrategie für den auszuführenden Code wird durch eine zugehörige Policy-Datei festgelegt, welche auf dem Rechner des Nutzers angelegt sein
muss [Ec08, 606 f.]. Fehlt diese Datei, wird der Code in der Sandbox ausgeführt.
Die Datei kann abhängig von Herkunftsort (z. B. Internetadresse) oder Signaturen
des Codes Rechte vergeben, wie bspw. den Lese- oder Schreibzugriff auf bestimmte lokale Dateien oder die Verbindungsherstellung zu bestimmten Internetadressen. Die Überprüfung von Signaturen erfolgt über einen Abgleich von dem privaten
Schlüssel, mit dem der Code signiert ist, mit einem öffentlichen Schlüssel, der in einer Schlüsseldatenbank auf dem ausführenden Rechner vorhanden sein muss [Go09,
Kap. 3.3].
Folgendes Code-Bsp. zeigt zwei mögliche Einträge in einer Policy-Datei (in Anlehnung an [Ec08, S. 607]). Der erste Eintrag erlaubt allen Klassen, die als Herkunft die URL ”https://www.rman.com/users/bob” haben und von ”Duke” signiert sind, den lesenden und schreibenden Zugriff auf alle Dateien im Verzeichnis
”/bob/temp/”, den lesenden Zugriff auf alle class-Dateien (gekennzeichnet durch
die Endung auf ”/” statt auf ”/*”) im Verzeichniss ”/joe/temp/” und die Verbindungsherstellung über ein Socket zu allen Adressen der Domäne ”rman.com”. Der
zweite Eintrag ermöglicht unbahängig von Herkunft und Signatur den Empfang von
Daten an allen Ports von 1024 aufwärts des lokalen Rechners.
grant codeBase "https://www.rman.com/users/bob", signedBy "Duke"{
permission java.io.FilePermission "read, write","/bob/temp/*";
permission java.io.FilePermission "read","/joe/temp/";
permission java.net.SocketPermision "connect","*.rman.com";
};
grant {permission java.net.SocketPermission
"localhost:1024-","listen";};
Erweitert wird die Sicherheitsstrategie um Schutzdomänen, welchen einzelne Klassen
und Rechte zugewiesen werden können. Die Rechte einer Schutzdomäne übertragen
sich damit auf die darin enthaltenen Klassen und deren Instanzen. Die einzige
Domäne mit dem Recht des freien Zugriffs auf Systemressourcen ist die Systemdomäne. Diese enthält die Klassenbibliotheken, welche die JVM umfasst und zur
13
Kapitel 3: Die Java Virtual Machine
Ausführung von Code benötigt. Diese Domäne ist zudem der einzige Weg, über den
unterschiedliche Domänen miteinander interagieren können. Auf direktem Wege ist
dies untersagt.
Die Zugriffskontrolle wird durch den Sicherheitsmanager durchgeführt, welcher hierzu um den Access Controller erweitert wird [Ec08, 607-609]. An diesen werden
die Zugriffsanfragen weitergeleitet und dieser überprüft anhand der entsprechenden
Policy-Datei, ob die anfragende Komponente die Rechte hierzu besitzt. Es besteht
allerdings auch die Möglichkeit, den Access Controller direkt anzusprechen, indem
diesem Policy-Datei und gewünschte Berechtigungen übergeben werden. Die Kontrolle des Access Controllers umfasst eine Analyse aller Zugriffe des Threads, in dem
die anfragende Komponente ausgeführt wird. Die erforderlichen Berechtigungen für
diese Anfrage müssen dabei allen Schutzdomänen, welche ein Thread bisher durchlaufen hat, zugeteilt sein. Dies soll verhindern, dass ein Thread bereits durch einen
einmaligen Zugriff auf die Systemdomäne freien Zugriff auf alle Systemressourcen
erlangt.
Eine weitere Möglichkeit zur Zugriffskontrolle bietet das Guarded Object. Dies
ermöglicht es dem Anbieter eines Objektes, dieses um ein Wächterobjekt zu erweitern. Dieses überwacht wiederum alle Zugriffe von Nutzern auf das zu bewachende
Objekt.
3.6.2
Risiken und Herausforderungen
Die Sicherheitskontrollen von Java sind mittlerweile schon umfangreich, nichtsdestotrotz bestehen nach wie vor noch Probleme und Herausforderungen für die weitere
Entwicklung. So ist die Verifizierung durch den Bytecode-Verifier lediglich eine Art
Unbedenklichkeitstest [Ec08, S. 604]. Ein Test auf die Funktionalität von Java-Code
ist nicht möglich.
Eine Angriffsmöglichkeit von außen ergibt sich durch das klar strukturierte classDateiformat. In eine bereits erstellte class-Datei lässt sich dadurch gezielt nachträglich Code in Form von JVM-Instruktionen schreiben, welche dann nur noch über
den Einbau des Befehls goto referenziert werden müssen. Zudem kann das Feld
access flags verändert werden, indem die Berechtigung darin verändert wird (z. B.
private löschen oder durch public ersetzen).
Der Befehl goto ist zudem ein gutes Beispiel für die nicht übereinstimmende
Semantik von der Sprache Java und dem Bytecode, welcher daraus generiert wird.
Dies ermöglicht die Erstellung einer class-Datei, welche so nicht von einem Java-
14
Kapitel 3: Die Java Virtual Machine
Compiler erstellt worden wäre, aber dennoch die Verifizierung vollständig durchläuft.
Über goto kann bspw. in Schleifen an beliebigen Punkten ein- und ausgestiegen
werden, was bei der Erstellung eines Programms in Java so nicht möglich ist. Dort
existieren feste Ein- und Austrittspunkte.
Die Rechtevergabe über die Policy-Datei stellt eine sinnvolle Weiterentwicklung der
Sicherheitsstrategie unter Java dar, nichtsdestotrotz kann auch sie keine vollständige
Sicherheit garantieren [Ec08, S. 609 f.]. Die Identifizierung von Java-Klassen und die
Vergabe von Rechten an diesen über deren Herkunftsadresse oder Signatur, welche
in der Policy-Datei angegeben sind, ermöglicht es Angreifern, eine Herkunftsadresse
vorzutäuschen und so potenziell gefährdenden Code mit zu viel Rechten auszuführen.
Eine Prüfung der Authentizität der Adresse einer zu ladenden Klasse wird vom Rechner nicht durchgeführt, sondern der für diese Klasse angegebenen wird vertraut und
einfach mit dem Eintrag in der Policy-Datei abgegelichen. Eine Verbesserung dieser
Authentifizierung oder der Einsatz sicherer Transportprotokolle können dabei zu
einer Problemlösung beitragen.
Die Nutzung von Signaturen kann ebenfalls ein Sicherheitsproblem darstellen.
Die Namen, deren Signaturen vertrauenswürdig sind, werden in einer lokalen Datenbank auf dem Rechner geführt. Die Pflege dieser Datenbank obliegt dabei alleine
dem Nutzer. Geschieht dies nicht, ist sowohl Konsistenz als auch Integrität der Datenbank unter Umständen nicht mehr sichergestellt.
Die Policy-Datei selbst kann auch das Ziel von Angriffen werden. Diese Datei
befindet sich auf dem Rechner des Nutzers, wodurch deren Integrität von den Sicherheitsmechanismen des Betriebssystem ist. Ein Angreifer kann ansonsten eine
eigene Policy-Datei auf dem ausführenden Rechner platzieren, um eigenen Code mit
allen von ihm gewünschten Rechten ausführen zu lassen. Durch diese Manipulation
könnte man das komplette Sicherheitskonzept umgehen.
Ein Schwäche stellt auch die Umsetzung der Zugriffskontrolle an sich dar. Diese
findet nur beim allerersten Objektzugriff durch Sicherheitsmanager und Access Controller statt. Bei allen folgenden Objektzugriffen werden diese nicht eingeschalten,
da für die bereits aufgerufenen Objekte Stellvertreter erzeugt werden, welche dem
Aufrufer übergeben werden. Dieser kann damit in der Folge ohne weitere Kontrolle
darauf zugreifen.
15
Kapitel 3: Die Java Virtual Machine
3.7
3.7.1
Die Squawk Java Virtual Machine
Die geteilte Architektur
Die Squawk Virtual Machine ist eine kleine JVM, die größtenteils in Java selbst
geschrieben und für den Einsatz auf drahtlosen Sensorgeräten, bei gleichzeitigem
Verzicht auf ein zusätzliches Betriebssystem, gedacht ist [SCCDW06, S. 78 f.]. Die
Idee der Entwickler dahinter ist die Schaffung von drahtlosen Sensornetzwerken im
alltäglichen Leben über den Einbau von Sensoren in bspw. Haushaltsgeräte, wie
etwa Waschmaschine und Herd. Sensorgeräte für diesen Einsatzzweck müssen von
geringer physischer Größe sein und können nur begrenzte Hardwareressourcen zur
Verfügung stellen. Aus diesem Grund werden zur Entwicklung von Anwendungen
hierfür in der Regel C und Assemblersprachen verwendet und nicht Java, da der
Speicherbedarf von JVM und den daraus auszuführenden Anwendungen im Normalfall deutlich größer ist. Der Einsatz von Sprachen, welche eine VM zur Ausführung
von Programmen nutzen und nicht direkt ausführbaren Maschinencode erstellen,
kann hingegen aufgrund einer kürzeren Entwicklungsdauer die Entwicklung von Anwendungen und Prototypen für Sensorgeräte vereinfachen. Für Java sprechen in
diesem Umfeld neben Garbage Collection, Sicherheit bei Zeigern, Fehlerbehandlung
und einer ausgereiften Verwaltung von Threads zudem vorhandene Entwicklungsumgebungen und Werkzeuge zum Debuggen und Deployen von Anwendungen.
Die Squawk VM ist so konzipiert, dass sie auf dem drahtlosen Sun Small Programmable Object Technology (SPOT) Gerät ohne zusätzliches Betriebssystem laufen
kann, wodurch Speicherplatz für dieses gespart werden kann [SCCDW06, S. 79].
Hierzu umfasst Squawk die Behandlung von Unterbrechungen (Interrupts), einen
Netzwerk-Stack und eine Ressourcenverwaltung. Diese Funktionen von Betriebssystemen, ein geringer Speicherbedarf, die Darstellung von Anwendungen als einzelne
Objekte, die Ausführung mehrerer Applikationen in einer VM, die Möglichkeit, Anwendungen direkt von einem Gerät zu einem anderen senden zu können, und die
Authentifizierung von Anwendungen auf dem Gerät nach dem Deployen machen
Squawk zu einer gut geeigneten VM für drahtlose Sensorgeräte.
Ein mögliches Einsatzgebiet für Geräte, die wie SPOT mit Sensoren ausgestattet
sind, ist bspw. die Ausstattung von Herd und Auto mit einem SPOT, um den Nutzer warnen zu können, wenn er mit dem Auto wegfährt und der Herd noch an ist
[SM09b]. Im industriellen Umfeld ist eine Sicherung von wertvollen Gütern hierüber
denkbar. So könnten sich z. B. Container gegenseitig überwachen, damit Alarm ge16
Kapitel 3: Die Java Virtual Machine
geben werden kann, wenn einer der Container geöffnet wird, obwohl sie sich noch
auf dem Transportweg befinden.
Der geringe Speicher kleiner, speicherarmer Geräte macht eine Veränderung der
gewöhnlichen VM-Architektur notwendig, da das Laden von Klassen nicht auf einem solchen Gerät durchgeführt werden kann [SCCDW06, S. 80]. Deshalb besitzt
Squawk eine verteilte Architektur (Vgl. Abb. 6), die sich zum einen aus der VM auf
dem Gerät selbst und zum anderen aus dem Suite Creator, welcher im Gegensatz zur
VM auf einem Desktop-Rechner läuft, zusammensetzt. Die Teile dieser Architektur,
welche auf dem Gerät laufen, sind nicht alle in Java geschrieben. Der einzige Teil
der VM, für den dies gilt, ist der Interpreter. Die Entwickler planen jedoch, diesen
in Zukunft in Java zu schreiben und dann in C konvertieren zu lassen. Auf dem
Gerät läuft zudem ein Bootloader, welcher in C implementiert ist. Dieser hat die
Aufgabe, alle Komponenten, die entweder in C geschrieben sind oder von Java in
C konvertiert werden, (z. B. Interpreter und Garbage Collector) und die BootstrapSuite auf das Gerät zu laden. Die Bootstrap-Suite ist eine suite-Datei, welche alle
Java-Bibliotheken (Java Libraries) umfasst, die für den Betrieb der Squawk VM
notwendig sind.
Host
Device
.class/.jar
.suite
Suite Creator
On-device VM
Loader
Interpreted VM
Verifier
Java libraries
Transformer
Bootloader
Serializer
Digital Signer
Native Code
.suite
Abbildung 6: Die geteilte Architektur der Squawk VM (weiß = Java-Code,
schwarz=C-Code), in Anlehnung an [SCCDW06, S. 80]
17
Kapitel 3: Die Java Virtual Machine
3.7.2
Das suite-Dateiformat
Der Suite-Creator hat die Aufgabe, class-Dateien in suite-Dateien zu konvertieren [SCCDW06, S. 80]. Diese Suite, die ebenfalls aus Bytecode, dem SquawkBytecode besteht, ist jedoch weniger speicheraufwändig und kann zudem weitere
Optimierungen umfassen. Hierzu gehören bspw. eine Kürzung von Instruktionen,
die normalerweise drei Bytes umfassen, auf 2 Bytes, wenn möglich, eine Auflösung
aller symbolischen Verweise auf andere Klassen, Methoden und Felder, wodurch der
Konstantenpool wegfalle und die spätere Interpretierung verschnellert werden kann
und eine Vereinfachung der Garbage Collection. Suite-Dateien haben dadurch im
Schnitt lediglich 38% der Größen von class-Dateien, ohne dabei jedoch komprimiert zu sein. Darauf wird bewusst verzichtet, um das Entpacken in der VM auf
dem Gerät zu vermeiden.
Eine zusätzliche Verringerung des geräteseitigen Speicheraufwands wird durch die
Einbettung des Squawk Bytecode Verifiers in den Suite Creator erreicht [SCCDW06,
S. 81]. Dieser muss sicherstellen, dass die Umwandlung von dem class- in das
suite-Dateiformat korrekt erfolgt ist. Im Wesentlichen entspricht dieser Verifier
dem gewöhnlichen Java Bytecode Verifier (Vgl. Kap. 3.4), wobei Anpassungen auf
den Squawk Bytecode jedoch zwangsläufig notwendig sind.
Eine Suite kann nicht nur einzelne Klassen, sondern ganze Klassenstrukturen enthalten [SCCDW06, S. 80]. Dabei kann eine Klasse immer nur auf Klassen der selben
Suite oder einer Elternsuite verweisen. Suites können, wie Klassen auch, hierarchisch
angeordnet werden. Der Squawk-Bytecode wird vor dem endgültigen Abspeichern
noch serialisiert und alle Zeiger auf andere Objekte werden in entsprechende Adressen übersetzt. Bei der Deserialisierung werden diese mittels einer Pointer-Tabelle,
welche ebenfalls in der suite-Datei enthalten ist, wieder aufgelöst. Die hierarchische
Anordnung von Suites und dieses Vorgehen beschleunigen den Start der VM und
das Laden von Klassen.
Die Möglichkeit zur Authentifizierung von Anwendungen, die auf ein Gerät übertragen
worden sind und ausgeführt werden sollen, ist bei der Architektur der geteilten VM
von großer Bedeutung [SCCDW06, S. 84]. Hierzu wird die Suite bei deren Erstellung
mit eine privaten Schlüssel signiert, welcher sich auf dem Desktop-Rechner befindet. Authentifiziert wird die Suite auf dem Gerät dann über den entsprechenden
öffentlichen Schlüssel. Ist dies erfolgreich, wird die Suite auf dem Gerät installiert.
18
Kapitel 3: Die Java Virtual Machine
3.7.3
Die geräteseitige VM
Die Absicht, die Squawk VM ohne Betriebssystem ausführen zu können, setzt voraus, dass Squawk einen eigenen Thread Scheduler umfasst, welcher die Ausführung
mehrerer Threads zur gleichen Zeit kontrolliert und für das Umschalten zwischen diesen verantwortlich ist [SCCDW06, S. 81 f.]. Hierzu setzt Squawk auf Green Threads,
welche eine Umgebung zur Ausführung mehrerer Threads emulieren, ohne dabei auf
ein Betriebssystem angewiesen zu sein.
Von Bedeutung ist beim Scheduling zudem die Behandlung von Unterbrechungen (Interrupts), welche sonst ebenfalls von einem Betriebssystem vorgenommen
wird. Hierbei muss beachtet werden, dass sowohl Garbage Collection als auch die
Ausführung von Systemcode nicht-präemptiv sind, also nicht durch Interrupts unterbrochen werden können. Dies beruht auf der Annahme, dass in Squawk die meiste
Zeit Anwendungen ausgeführt werden. Die Durchführung einer Unterbrechung erfolgt durch den Gerätetreiber, welcher in Java selbst geschrieben ist. Erfolgt eine
Unterbrechung, wird der aktuelle Thread gestoppt, weitere Unterbrechungen für
diesen werden gesperrt und der Punkt, an dem die Unterbrechung erfolgt ist, wird
gespeichert, um später wieder zu diesem zurückkehren zu können.
Die geteilte Architektur der Squawk ermöglicht das Debugging von Anwendungen über Werkzeuge, die das Java Debug Wire Protocol (JDWP) unterstützen (so
z. B. die Entwicklungsumgebung NetBeans von Sun Microsystems) [SCCDW06, S.
84]. Auf Seite des Desktop-Rechners läuft ein Debug Proxy und auf dem tragbaren Gerät ein Debug Isolate. Dieses umfasst zum einen den Squawk Debug Agent
(SDA), welcher für die Kommunikation mit dem Debug Proxy und die Steuerung
der Anwendung, welche debugged wird, zuständig ist und zum anderen die SDA
VM Unterstützung, welche Teil der VM ist und den SDA an diese anbindet. Die
Kommunikation erfolgt nicht über das JDWP, sondern über das daran angelehnte
Squawk Debug Wire Protocol (SDWP).
Die Ausführung und Kontrolle von Anwendungen in Squawk erfolgt durch die Repräsentierung dieser in Objekten, welche eine Instanz der Klasse Isolate darstellen
[SCCDW06, S. 82]. Über Methoden dieser Klasse kann eine Anwendung dann bspw.
gestartet und gestoppt werden Dies verdeutlicht folgendes Bsp., in welchem ein
Isolate zur Anwendung com.sun.spots.SelfHibernator instanziiert, das Isolate
gestartet und an dieses eine Ausgabedatenstrom gesendet wird:
19
Kapitel 3: Die Java Virtual Machine
Isolate isolate = new Isolate ("com.sun.spots.SelfHibernator", url());
isolate.start();
send (isolate, outStream);
...
Diese Isolates, welche eine Anwendung im Kontext der Squawk darstellen, verhalten
sich analog zu Prozessen in Betriebssystemen, indem ein Isolate mehrere Threads
enthalten kann und diesen gemeinsame Ressourcen zur Verfügung stellt [SCCDW06,
S. 82]. Einzelne Anwendungen laufen immer isoliert von den anderen ab. Anwendungsspezifische Daten, wie etwa Klassenvariablen, werden dabei im entsprechenden
Isolate-Objekt gespeichert. Die laufenden Anwendungen teilen sich im Gegensatz
zu sonstigen Implementierungen einer JVM allerdings allgemeine Ressourchen, wie
etwa die Java-Bibliotheken der VM. Es ist somit nicht eine VM für jede laufende
Anwendung notwendig, wodurch der Speicheraufwand verringert wird.
Isolates können in jedem Zustand zu einem Datenstrom serialisiert werden, um diese
z. B auf einer Festplatte speichern zu können [SCCDW06, S. 82]. Dabei werden auch
alle Threads inkl. aktuellem Zustand und temporären Variablen serialisiert. Vor der
Serialisierung muss ein Isolate jedoch zunächst alle offenen Verbindungen nach außen
hin, wie etwa Desktop-Rechner, schließen. Der entstehende Datenstrom kann direkt
in eine andere Squawk VM eingelesen werden. Zur Vereinfachung hiervon, können
Isolates auch direkt von einem Gerät auf ein anderes migriert werden. Anwendungen
können somit in einem Zustand angehalten werden und auf dem selben Gerät oder
jedem beliebigen anderen fortgesetzt werden. Über den Befehl moveTo(IPAddress
ip) können sich Anwendungen auch selbstständig auf ein anderes Gerät verschieben.
20
Kapitel 4: Fazit und Ausblick
4
Fazit und Ausblick
Mit der JVM als Kernstück der Java-Architektur können die Entwicklungsziele von
Java weitestgehend umgesetzt werden. Die Übersetzung von Java-Code in Bytecode
und dessen Speicherung in class-Dateien ermöglicht in Verbindung mit der JVM
die Portabilität einer einmal übersetzten Java-Applikation und deren Ausführung
auf jedem beliebigen Zielrechner.
Für die Sicherheit sorgen dabei die umfassenden Mechanismen zur Verifizierung
von class-Dateien und Zugriffskontrolle. Diese haben sich seit den Anfängen von
Java zwar deutlich verbessert, gewährleisten aber nach wie vor keinen hundertprozentigen Schutz vor bösartigen Angriffen von außen.
Die Implementierung der Squawk VM gibt ein gutes Bsp. für die Möglichkeiten,
die einem Java mittlerweile gibt. So etwa die Implementierung einer JVM in Java
selbst, die auf bestimmten Geräten (z. B. dem SPOT) sogar ohne seperates Betriebssystem lauffähig ist. Squawk und SPOT geben zudem einen guten Ausblick darauf,
was im Bereich der Java-Entwicklung in Zukunft kommen kann.
Java und damit auch die JVM sind generell einem ständigen Veränderungsprozess
unterworfen, welcher u. a. die Steigerung der Performance zum Ziel hat [SM07].
Von großer Bedeutung ist dabei eine stetige Verbeserung der Garbage Collection.
So umfasst die Java HotSpot VM mittlerweile bspw. unterschiedliche Garbage Collector, welche je nach Größe der auszuführenden Applikation und zu erzielender
Performance geeignet und dementsprechend ausgewählt werden können [SM09c].
Ein aktueller Änderungsvorschlag über ein Java Specification Request (JSR)
existiert derzeit nur ein einziger in JSR 924 [SM04]. Dieser soll die Typüberprüfung
(Typechecking) für alle class-Dateien, deren Versionsnummer größer als 50 ist,
einführen.
Weitere Anpassungen der aktuellen Spezifikation von Java und der JVM sind
aber für die Zukunft zu erwarten, um die Sicherheit und Performance von Java
weiter steigern zu können.
21
Kapitel A: Übersetzung von Java-Code in Bytecode
A
Übersetzung von Java-Code in Bytecode
Startprogramm in Java-Code
Zwischenprogramm in Bytecode
class vector {
aload 0
Load this
int arr[];
getfield #10
Load this.arr
int sum() {
astore 1
Store in la
int sum() {
iconst 0
int la[] = arr;
istore 2
Store 0 in S
int S = 0;
aload 1
Load la
for (int i=la.length; −−i>=0;)
arraylength
Get its length
istore 3
Store in i
iinc 3 -1
Subtract 1 from i
iload 3
Load i
iflt B
Exit loop if ¡0
iload 2
Load S
aload 1
Load la
iload 3
Load i
iaload
Load la[i]
iadd
add in S
istore 2
store to S
goto A
do it again
iload 2
Load S
ireturn
Return it
S += la[i];
return S;
A:
}
}
B:
Tabelle 1: Übersetzung eines Startprogramms in Java-Code in ein Zwischenprogramm in Bytecode (Vgl. [Go95, S. 112])
22
Literaturverzeichnis
Literatur
[ALUS08] Alfred V. Aho, Monica S. Lam, Jeffrey D. Ullman, Ravi Sethi: Compiler:
Prinzipien, Techniken und Werkzeuge, 2nd Edition, Pearson Education, 2008.
[Ec08] Claudia Eckert: IT-Sicherheit – Konzepte-Verfahren-Protokolle, 5. Auflage,
Oldenbourg Wissenschaftsverlag, 2008.
[DN07] Klaus-Georg Deck und Herbert Neuendorf: Java-Grundkurs für Wirtschaftsinformatiker, 1. Auflage, Vieweg+Teubner, 2007.
[Go09] Li Gong: Java SE Platform Security Architecture, Version 1.2,
URL: http://java.sun.com/javase/6/docs/technotes/guides/security/
spec/security-spec.doc.html,
Abrudatum: 17. Mai 2009.
[Go95] James Gosling: Java Intermediate Bytecodes, Papers from the 1995 ACM
SIGPLAN workshop on Intermediate representations, S. 111-118, 1995.
[GJSB05] James Gosling, Bill Joy, Guy Steele, Gilad Bracha: The Java Lanuage
Specification, 3rd Edition, Addison-Wesley Longman, 2005,
abgerufen über:
URL: http://java.sun.com/docs/books/jls/
java language-3 0-mr-spec.zip,
Abrufdatum: 11. Mai 2009.
[Ke01] Christian Kenngott: Virtuelle Maschinen mit erweiterbarem Befehlssatz,
Dissertation am Institut für Praktische Informatik (Gruppe Software) der
Technisch-Naturwissenschaftlichen Fakultät der Johannes Kepler Universität
Linz, 2001.
[LY99] Tim Lindholm, Frank Yellin: The Java Virtual Machine Specification, 2nd
Edition, Addison-Wesley Longman, 1999,
abgerufen über:
URL: http://java.sun.com/docs/books/jvms/second edition/
html/VMSpecTOC.doc.html,
Abrufdatum: 04. März 2009.
unter Einbeziehung von:
URL: http://java.sun.com/docs/books/jvms/jvms-maintenance.html,
Abrufdatum: 21. April 2009.
23
Literaturverzeichnis
[Re06] Peter Rechenberg: Informatik-Handbuch, 4. Auflage, Hanser, 2006.
[SCCDW06] Doug Simon, Cristina Cifuentes, Dave Cleal, John Daniels, Derek White: Java on the Bare Metal of Wireless Sensor Devices: The Squawk Java Virtual
Machine, VEE ’06: Proceedings of the 2nd international conference on Virtual
execution environments, S. 78-88, ACM, 2006.
[SM04] Sun Microsystems: JSR-000924 Java Virtual Machine Specification,
URL: http://jcp.org/aboutJava/communityprocess/maintenance/
jsr924/index2.html
Abrufdatum: 10. Mai 2009.
[SM07] Sun Microsystems: Java SE 6 Performance White Paper,
URL: http://java.sun.com/performance/reference/whitepapers/
6 performance.html,
Abrufdatum: 17. Mai 2009.
[SM09a] Sun Microsystems: Java SE HotSpot at a Glance,
URL: http://java.sun.com/javase/technologies/hotspot/,
Abrufdatum: 17. Mai 2009.
[SM09b] Sun Microsystems: Sun Small Programmable Object Technology – Applications for Sun SPOTs,
URL: http://www.sunspotworld.com/vision.html,
Abrufdatum: 11. Mai 2009.
[SM09c] Sun Microsystems: Java SE 6 HotSpot Virtual Machine Garbage Collection
Tuning,
URL: http://java.sun.com/javase/technologies/hotspot/gc/
gc tuning 6.html,
Abrufdatum: 17. Mai 2009.
[Ve00] Bill Venners: Inside The Java Virtual Machine, 2nd Edition, McGraw-Hill
Companies, 2000,
abgerufen über:
URL: http://www.artima.com/insidejvm/blurb.html,
Abrufdatum: 11. Mai 2009.
24