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