Рефетека.ру / Информатика и програм-ие

Курсовая работа: Ассемблер для платформы Java

Содержание.


Содержание.

Содержание.

Введение.

Постановка задачи.

Формат файла класса.

Структура файла класса.

Типы элементов Constant_pool

Формат структуры field_info

Формат структуры method_info

Формат атрибута Code.

Работа JVM

Система команд JVM.

Синтаксис языка ассемблера для платформы Java (языка JASM).

Тестовые примеры.

1.

2.

3.

Проектирование и реализация компилятора.

Заключение.

Использованная литература.

Введение.


Язык программирования Java был разработан в середине 90-х годов на основе языка Oak, предназначавшегося для программирования «прошивок» для различных электронных устройств. Однако, в отличие от своего предшественника, язык Java получил широкое распространение, прежде всего как язык, использовавшийся в программировании для сети Интернет. В настоящее время область применения Java значительно расширилась, и этот язык часто применяется и в обычном прикладном программировании. Это обусловлено такими преимуществами как кроссплатформенность и безопасность, которые обеспечиваются тем, что исходный код на Java компилируется не непосредственно в машинный код, а в, так называемый, байт-код, который интерпретируется виртуальной машиной Java (JVM). Во многих современных реализациях JVM байт-код перед выполнением преобразуется в машинные инструкции, что значительно повышает производительность, приближая ее к производительности программ, написанных на C/C++. Таким образом, Java, в современном состоянии этой технологии, сочетает преимущества интерпретируемых и компилируемых языков программирования.

Спецификация, описывающая JVM, как абстрактную вычислительную машину, предоставлена компанией Sun в открытый доступ. Это позволяет создавать как собственные реализации JVM для различных платформ, так и альтернативные компиляторы, генерирующие байт-код Java, в том числе для языков программирования, отличных от Java. Большинство литературы, посвященной Java, почти не уделяет внимания устройству JVM и описывает лишь сам язык Java. Однако, в ряде случаев, знание особенностей архитектуры бывает весьма полезным. В данной работе я создал учебную программу, которая может помочь в изучении архитектуры JVM – несложный ассемблер для байт-кода Java.

Постановка задачи.


Требуется изучить архитектуру уровня команд платформы Java, формат файла класса Java, и написать компилятор ассемблероподобного языка, позволяющего создавать файлы классов, корректно обрабатываемые реальной JVM. Данный компилятор должен поддерживать все команды байт-кода Java и важнейшие возможности JVM.

Формат файла класса.


Основным форматом исполняемых файлов в архитектуре Java является формат файла класса, описанный в The JavaTM Virtual Machine Specification, изданной компанией Sun. Файл данного формата имеет имя, совпадающее с идентификатором класса (за исключением вложенных классов) и расширение .class.


Структура файла класса.


Файл класса имеет следующую структуру:


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;

     method_info methods[methods_count];

     u2 attributes_count;

     attribute_info attributes[attributes_count];

    }

(здесь и далее u1, u2, u4 – целые числа размером 8, 16 и 32 бит с порядком байтов старший байт по младшему адресу). Рассмотрим последовательно все поля.

  • magic – так называемое магическое число, имеющее в шестнадцатеричной записи вид 0xCAFEBABE;

  • minor_version, major_version – версия формата файла, по ней определяется совместимость данного файла с конкретной версией JVM;

  • constant_pool_count – количество элементов в Constant_pool плюс единица;

  • constant_pool – область констант – массив структур переменного размера, представляющих те или иные константные значения. Обращения в область констант производятся по индексу (индексация начинается с единицы; индексы, следующие за позициями констант, представляющих числа типов long и double, не используются). Форматы констант различных видов будут рассмотрены ниже;

  • access_flags – комбинация битовых флагов, определяющих права доступа и некоторые другие характеристики класса:

Флаг

Значение

Смысл

ACC_PUBLIC

0x0001

Доступен из-за пределов пакета

ACC_FINAL

0x0010

Запрещено наследование от данного класса

ACC_SUPER

0x0020

В методах данного класса требуется использовать принятую в Java2 трактовку команды invokespecial

ACC_INTERFACE

0x0200

Интерфейс (является классом специального вида)

ACC_ABSTRACT

0x0400

Абстрактный класс

  • this_class, super_class – индексы структур в области констант, ссылающихся на данный класс и его класс-предок;

  • interfaces_count – число интерфейсов, реализуемых данным классом;

  • interfaces – массив индексов структур в области констант, ссылающихся на интерфейсы, реализуемые данным классом;

  • fields_count – количество полей в данном классе;

  • fields – массив структур field_info, описывающих поля класса. Формат структуры field_info будет рассмотрен ниже;

  • methods_count – количество методов;

  • methods – массив структур method_info, описывающих методы класса. Формат структуры mettho_info будет рассмотрен ниже. Конструкторы и статические инициализаторы представляются методами со специальными именами <init> и <clinit>;

  • attributes_count – количество атрибутов класса;

  • attributes – массив структур-атрибутов класса (поля, методы и байт-код методов также могут иметь свои атрибуты). Каждая такая структура в начале имеет два обязательных поля, описывающих тип атрибута и его размер. К классу могут быть применены следующие стандартные атрибуты: SourceFile – указывает на файл исходного текста, из которого был получен данный файл класса, и Deprecated – класс оставлен для совместимости со старым кодом и его использование не рекомендуется. Возможно создание атрибутов нестандартных типов, но они будут игнорироваться средой выполнения.


Типы элементов Constant_pool


Каждый элемент сonstant_pool начинается с однобайтного поля, определяющего его тип. Размер и содержание остальной части структуры зависит от типа. Существуют следующие типы констант (элементов constant_pool):

  • CONSTANT_Class – указывает на класс. Содержит индекс константы типа CONSTANT_Utf8, хранящей дескриптор класса;

  • CONSTANT _Fieldref – указывает на поле класса. Содержит индексы констант типа CONSTANT_Class и CONSTANT_NameAndType;

  • CONSTANT _Methodref указывает на метод класса (не интерфейса). Содержит индексы констант типа CONSTANT_Class и CONSTANT_NameAndType;

  • CONSTANT _InterfaceMethodref указывает на метод интерфейса. Содержит индексы констант типа CONSTANT_Class и CONSTANT_NameAndType;

  • CONSTANT_String – указывает на строку, содержит индекс константы типа CONSTANT_Utf8;

  • CONSTANT_Integer – содержит целое 32-разрядное число;

  • CONSTANT_Float – содержит вещественное число одинарной точности;

  • CONSTANT_Long – содержит целое 64-разрядное число;

  • CONSTANT_Double – содержит вещественное число двойной точности;

  • CONSTANT_NameAndType – описывает сигнатуру и имя метода либо тип и имя поля. Содержит индексы двух констант типа CONSTANT_Utf8, хранящих соответственно имя и дескриптор типа (сигнатуры);

  • CONSTANT_Utf8 – содержит строку в формате Utf8 (символы Unicode представляются комбинациями от 1 до 3-х байт, причем символы с кодами, не превышающими 127, представляются одним байтом).


Дескрипторы – это строки, описывающие типы и сигнатуры методов в компактном формате. Примитивные типы обозначаются одной буквой, типы массивов – открывающими квадратными скобками в количестве, равном размерности массива, перед обозначением базового типа. Классы описываются строкой, содержащей имя класса с полным путем, при этом вместо точки роль разделителя имен пакетов и класса выполняет слэш. В дескрипторах сигнатур методов в круглых скобках без разделителей перечисляются дескрипторы типов параметров; после закрывающей скобки находится дескриптор типа возвращаемого значения. Для устранения неоднозначностей при этом перед дескрипторами классов записывается буква L, а после них – точка с запятой. Например, (ILjava/lang/Object;)I – (int, Object):int (буквой I обозначается тип int).


Формат структуры field_info


Структура field_info имеет следующий формат:


field_info {

     u2 access_flags;

     u2 name_index;

     u2 descriptor_index;

     u2 attributes_count;

     attribute_info attributes[attributes_count];

     }

Здесь:

  • access_flags - комбинация битовых флагов, определяющих права доступа и некоторые другие характерист ики поля:

    Имя флага

    Значение

    Смысл

    ACC_PUBLIC

    0x0001

    Поле объявлено как public

    ACC_PRIVATE

    0x0002

    Поле объявлено как private

    ACC_PROTECTED

    0x0004

    Поле объявлено как protected

    ACC_STATIC

    0x0008

    Поле является статическим

    ACC_FINAL

    0x0010

    Поле объявлено как final и не может быть изменено после начальной инициализации

    ACC_VOLATILE

    0x0040

    Поле объявлено как volatile

    ACC_TRANSIENT

    0x0080

    Поле объявлено как transient – не сохранятся при сериализации

  • name_index – индекс строковой константы-имени поля в Constant Pool;

  • descriptor_index – индекс строковой константы-дескриптора поля (описывает тип) в Constant Pool;

  • attributes_count – число атрибутов поля;

  • attributes – атрибуты поля. К полям могут быть применены стандартные атрибуты Deprecated (см. выше), Synthetic (поле создано компилятором и не объявлено явно в исходном тексте) и ConstantValue (инициализирующее значение для статического поля).

Формат структуры method_info

Структура method_info имеет следующий формат:


 method_info {

     u2 access_flags;

     u2 name_index;

     u2 descriptor_index;

     u2 attributes_count;

     attribute_info attributes[attributes_count];

     }


Здесь:

  • access_flags – битовые флаги, определяющие права доступа и некоторые дополнительные свойства метода:


Flag Name

Value

Interpretation

ACC_PUBLIC

0x0001

Метод объявлен как public

ACC_PRIVATE

0x0002

Метод объявлен как private

ACC_PROTECTED

0x0004

Метод объявлен как protected

ACC_STATIC

0x0008

Метод является статическим

ACC_FINAL

0x0010

Метод является финальным и не может быть замещен

ACC_SYNCHRONIZED

0x0020

Метод объявлен как synchronized

ACC_NATIVE

0x0100

Метод является «родным» и содержит код, непосредственно выполняющийся физическим процессором

ACC_ABSTRACT

0x0400

Метод является абстрактным

ACC_STRICT

0x0800

Устанавливает «строгий» режим работы с вещественными числами (только в Java 2).


  • name_index, descriptor_index, attributes_count – аналогично field_info;

  • attributes – атрибуты метода. Методы могут иметь следующие стандартные атрибуты:

    • Deprecated, Synthetic – аналогично соответствующим атрибутам полей;

    • Exceptions – описание исключений, которые может генерировать метод. Нужно отметить, что обязательное описание исключений не является необходимым требованием для корректного выполнения;

    • Code – собственно говоря, байт-код метода.


Формат атрибута Code.


Атрибут Code имеет следующую структуру:


Code_attribute {

     u2 attribute_name_index;

     u4 attribute_length;

     u2 max_stack;

     u2 max_locals;

     u4 code_length;

     u1 code[code_length];

     u2 exception_table_length;

     { u2 start_pc;

     u2 end_pc;

     u2 handler_pc;

     u2 catch_type;

     } exception_table[exception_table_length];

     u2 attributes_count;

     attribute_info attributes[attributes_count];

    }


Здесь:

  • attribute_name_index, attribute_length – стандартные для любого атрибута поля, описывающие его тип и размер;

  • max_stack – предельный размер стека операндов для метода;

  • max_locals – предельное количество локальных переменных метода (включая формальные параметры);

  • code_length – размер байт-кода метода в байтах;

  • code – собственно говоря, байт-код;

  • exception_table_length – количество защищенных блоков;

  • exception_table – таблица защищенных блоков (обработчиков исключений). Каждая ее запись имеет следующие поля:

    • start_pc – индекс начала защищенного блока в массиве байт-кода,

    • end_pc – индекс конца защищенного блока,

    • handler_pc – индекс начала обработчика,

    • catch_type – тип обрабатываемого исключения (индекс в Constant Pool) или 0 для блока try ... finally;

  • attributes_count – число атрибутов;

  • attributes – атрибуты кода метода. Могут использоваться стандартные атрибуты LineNumberTable и LocalVariableTable, содержащие отладочную информацию.


Работа JVM


При запуске JVM в качестве параметров ей передаются имя класса, с метода main которого будет начато выполнение программы, а также аргументы командной строки программы. Вначале загружается указанный класс. Другие классы, используемые в программе, загружаются при первом обращении к ним. Процесс загрузки класса состоит из нескольких этапов:

  • собственно загрузка файла класса (loading). По умолчанию осуществляется с помощью класса ClassLoader из стандартной библиотеки Java, однако можно использовать пользовательский загрузчик для изменения способа поиска файла;

  • связывание (linking). Состоит из трех стадий:

    • проверка (verification) на правильность формата файла класса и корректность байт-кода (например, на отсутствие переходов на середину инструкции),

    • подготовка (preparation) – выделение памяти для статических полей класса и заполнение их нулевыми значениями,

    • разрешение имен (resolution) ;

  • инициализация (initialization) статических данных начальными значениями. Включает вызов метода <clinit>, если он присутствует в классе.

Программа, выполняемая JVM, может иметь несколько потоков выполнения. Реализация многопоточности зависит от используемого аппаратного обеспечения и может быть различной – разные потоки могут выполняться на разных процессорах или им могут выделяться кванты времени на одном процессоре. JVM имеет ряд средств для синхронизации работы потоков и защиты разделяемых ими данных. Важнейшим из них является механизм блокировок (locks), поддерживаемый на уровне системы команд JVM. Каждый объект имеет ассоциированный с ним «замок» (lock). Если один из потоков «закрыл» этот «замок», то ни один другой поток не сможет также его «закрыть» до тех пор, пока первый поток его не «откроет».

JVM определяет несколько виртуальных областей памяти, которые она использует при своей работе:

  • регистр PC (program counter), указывающий на текущую позицию выполнения в методе. Каждый поток программы имеет свой регистр PC;

  • стек. Каждый поток имеет свой собственный стек. При входе в метод на вершине стека создается фрейм, содержащий локальные переменные метода и его стек операндов. Размер именно этих областей указывается полями max_locals и max_stack атрибута метода Code;

  • куча – область памяти, в которой динамически размещаются объекты классов и массивы. Память из-под не используемых более объектов (на которые нет ссылок) автоматически освобождается так называемым сборщиком мусора;

  • область методов. В нее при загрузке классов помещаются байт-код методов, различная информация о методах и полях. Область методов также содержит области констант времени выполнения, которые хранят содержимое constant pool из загруженных классов;

  • стеки для native-методов.

Расположение и представление этих областей в физической памяти может быть различным в различных реализациях JVM.

JVM является стековой машиной. Большинство из команд JVM выполняют одно из следующих действий:

  • считывают значение из переменной или поля и помещают его на вершину стека,

  • сохраняют значение с вершины стека в переменной или поле,

  • выполняют те или иные действия над значениями, взятыми с вершины стека, и записывают результирующее значение (если оно есть) на вершину стека,

  • выполняют переход на команду с заданным смещением относительно текущего значения регистра PC безусловно или в зависимости от значений, прочитанных с вершины стека.

Любое чтение из стека операндов приводит к удалению из него прочитанного значения. Размер стека операндов, указываемый как max_stack, рассчитывается следующим образом: значения типов long и double занимают две ячейки стека (8 байт), любые другие значения – одну (4 байта). Значения типов char, boolean, byte, short сохраняются в одной четырехбайтной ячейке. Тут можно отметить, что в подавляющем большинстве случаев JVM не делает различий между логическими значениями и целыми числами типа int, для среды выполнения не существует отдельного булевского типа (лжи соответствует нулевое значение, истине – ненулевое, как правило, единица). Однако, в массивах типа boolean[] на каждый элемент выделяется один байт. Существует следующее ограничение на байт-код: каждый раз, когда точка выполнения достигает любой конкретной позиции в методе, глубина стека должна быть одинаковой, кроме того, тип верхних значений в стеке должен соответствовать требуемому типу извлекаемых очередной командой значений.

В области локальных переменных на момент начала выполнения метода в первых позициях находятся фактические параметры метода, а в случае метода экземпляра первую (нулевую) позицию занимает ссылка this на текущий объект. Никакого различия в процессе выполнения метода между параметрами (даже ссылкой this) и, собственно говоря, локальными переменными не делается. Так же как и в стеке, значения типа long и double в области локальных переменных занимают две четырехбайтные ячейки, значения типов размером менее 32-х разрядов расширяются до четырех байт. В корректном байт-коде должны выполняться, в частности, следующие условия: во-первых, типы значений в локальных переменных должны соответствовать требуемым для команд, которые обращаются к этим переменным, во-вторых, не допускается чтение значения переменной до ее инициализации (присвоения значения).

Перед вызовом метода его фактические параметры должны находиться на вершине стека; они становятся операндами соответствующей команды вызова. При возврате из метода, за исключением методов, возвращающих void, возвращаемое значение помещается на вершину стека.

В процессе выполнения программы в результате возникновения той или иной ошибки либо выполнения команды athrow может быть сгенерировано исключение. При этом происходит поиск подходящего обработчика исключения (защищенного блока) в текущем методе, если он не найден, то в методе, вызвавшем текущий и т. д. Если подходящий обработчик найден, то управление передается в точку, определяемую полем handler_pc соответствующей записи таблицы exception_table в атрибуте Code метода. Ссылка на объект исключения при этом помещается на вершину стека. Объект исключения обязательно должен принадлежать классу Throwable или классу, производному от него.

Система команд JVM.


Первый байт каждой команды JVM содержит код операции. Существует несколько типичных форматов, которые имеют большинство команд:

  • только код операции,

  • код операции и однобайтный индекс,

  • код операции и двухбайтный индекс,

  • код операции и двухбайтное смещение перехода,

  • код операции и четырехбайтное смещение перехода.

Несколько команд используют другие форматы, среди них две команды переменного размера - tableswitch и lookupswitch. Кроме того, существует специальный префикс wide, который изменяет размер некоторых команд, заменяя однобайтный индекс локальной переменной двухбайтным. В The Java Virtual Machine Specification для каждой команды установлено свое мнемоническое обозначение.

Существует много групп аналогичных по выполняемому действию команд, работающих с различными типами данных, например, команды iload, lload, aload, fload, dload выполняют функцию загрузки значений соответствующих типов из локальных переменных на вершину стека. Реализация таких команд может быть идентичной, но он различаются при проверке корректности байт-кода. Приняты следующие обозначения для типов данных, с которыми работают команды:

  • i - int (также byte, short, char и boolean),

  • l - long,

  • f - float,

  • d - double,

  • a - ссылка на объект или массив.

Кроме того, есть несколько команд, работающих с типами char, byte и short.

Можно выделить несколько групп команд по назначению:

  • команды загрузки и сохранения:

    • Загрузка локальной переменной на стек: iload, iload_<n>, lload, lload_<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n>;

    • Сохранение значения с вершины стека в локальной переменной: istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>;

    • Загрузка констант на стек: istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>;

  • арифметические и логические команды:

    • сложение: iadd, ladd, fadd, dadd;

    • вычитание: isub, lsub, fsub, dsub;

    • умножение: imul, lmul, fmul, dmul;

    • деление: idiv, ldiv, fdiv, ddiv;

    • остаток: irem, lrem, frem, drem;

    • изменение знака: ineg, lneg, fneg, dneg;

    • сдвиги и побитовые операции: ior, lor, iand, land, ixor, lxor, ishl, ishr, iushr, lshl, lshr, lush;

    • сравнение: dcmpg, dcmpl, fcmpg, fcmpl, lcmg;

    • инкремент локальной переменной: iinc.

Все эти команды, за исключением iinc, не имеют параметров. Они извлекают операнды с вершины стека и записывают результат на вершину стека. Команда iinc имеет два операнда - индекс локальной переменной и величину, на которую значение данной переменной должно быть изменено;

  • команды преобразования типов:

    • расширяющее: i2l, i2f, i2d, l2f, l2d, f2d;

    • сужающее: i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, d2f;

  • команды работы с объектами и массивами:

    • создание объекта: new;

    • создание массива: newarray (примитивного типа), anewarray (ссылочного типа), multianewarray(многомерного);

    • доступ к полям: getfield, putfield (для полей экземпляра), getstatic, putstatic (для статических полей);

    • загрузка элемента массива на стек: baload (тип byte), caload (тип char), saload (тип short), iaload, laload, faload, daload, aaload;

    • сохранение значения с вершины стека в элемент массива: bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore;

    • получение размера массива: arraylength;

    • проверка типов: instanceof (возвращает на вершине стека логическое значение) и checkcast (генерирует исключение в случае несоответствия типа ссылки на вершине стека требуемому типу);

  • команды манипуляций со стеком операндов:

    • pop - удаление верхнего элемент стека;

    • pop2 - удаление двух верхних элемента стека;

    • dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2 - дублирование элементов на вершине стека;

    • swap - перемена местами двух верхних элементов стека;

  • команды безусловной передачи управления:

    • jsr, jsr_w, ret - вызов подпрограмм и возврат из них. Используются при компиляции блока finally;

    • goto, goto_w - безусловный переход;

  • команды условного перехода: ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmpgt, if_icmple, if_icmpge, if_acmpeq, if_acmpne;

  • команды вызова методов:

    • invokevirtual - обычный вызов метода экземпляра с использованием механизма позднего связывания;

    • invokestatic - вызов статического метода;

    • invokeinterface - вызов метода интерфейса у объекта, реализующего данный интерфейс;

    • invokespecial - вызов метода экземпляра без использования механизма позднего связывания. Используется для вызова конструкторов, методов суперкласса и private-методов;

  • команды возврата из метода:

    • return - возврат из метода, возвращающего void;

    • ireturn, lreturn, freturn , dreturn, areturn - возврат значения соответствующего типа;

  • команда генерации исключений: athrow;

  • команды синхронизации (механизм блокировок):

    • monitorenter - установить блокировку (войти в критическую секцию);

    • monitorexit - освободить блокировку (выйти из критической секции).

Синтаксис языка ассемблера для платформы Java (языка JASM).


Файл исходного текста на языке ассемблера для платформы Java (языке JASM) представляет собой текстовый файл, строки которого разделены последовательностью символов с кодами 13 и 10. Имя файла исходного текста и его расширение могут быть любыми, однако рекомендуется, чтобы имя совпадало с именем описанного в файле класса, а расширением было .jasm либо .jsm. Файл исходного текста состоит из предложений, разделенных точкой с запятой. Последнее предложение может не иметь в конце точки с запятой. Комментарии отделяются знаком процента и распространяются до конца строки. Точки с запятой и знаки процента внутри строковых констант, ограниченных двойными кавычками, не имеют своего специального значения. Две идущие подряд двойные кавычки внутри строковой константы интерпретируются как одна двойная кавычка в строке. Любые последовательности пробельных символов (пробелов, табуляций, переводов строки и т. д.) интерпретируются как один пробел, если с обеих сторон от них находятся символы следующих видов: буквы латинского алфавита, цифры, знак подчеркивания, либо, в противном случае, игнорируются. Исключение составляют пробельные символы в строковых константах и комментариях. Верхний и нижний регистр букв в идентификаторах, именах команд и других лексемах различается.

Каждый файл исходного текста компилируется в один файл класса. Файл исходного текста должен иметь следующую структуру:


[модификаторы_доступа] {class|interface} <имя_класса>;

[extends <базовый класс>;]

[implements <интерфейс_1>, <интерфейс_2>, ... , <интерфейс_n>;]

[fields;

<описания_полей>

]

[methods;

<описания_методов>

]


Здесь и далее в квадратные скобки заключены необязательные элементы, в фигурные - альтернативные варианты (разделены вертикальной чертой), в угловые - нетерминальные символы.

Модификаторы_доступа - это слова public, final, abstract, super, соответствующие флагам прав доступа ACC_PUBLIC, ACC_FINAL, ACC_ABSTRACT, ACC_STATIC. Эти флаги устанавливаются в единицу тогда и только тогда, когда в объявлении класса присутствует соответствующее ключевое слово. Класс может иметь несколько различных модификаторов доступа, разделенных пробелом (или любой другой последовательностью пробельных символов). Повторение одинаковых модификаторов в заголовке одного класса не допускается. Когда класс не имеет флага ACC_INTERFACE, в его объявлении используется слово class, иначе используется ключевое слово interface. Все имена классов и интерфейсов записываются с указанием полного пути (пакетов, в которых эти классы содержатся). Имена пакетов и класса отделяются точкой, например, java.lang.String. В аргументах команд, там, где это необходимо, вместо полного имени текущего класса можно использовать символ «@». Если базовый класс не указан (предложение extends отсутствует), то по умолчанию используется java.lang.Object. Интерфейсы - предки описываемого интерфейса записываются в секции implements.

Для идентификаторов - имен пакетов, классов, полей и методов, а также меток, используются следующие правила: они должны состоять из букв латинского алфавита любого регистра (регистр имеет значение), знаков подчеркивания и цифр, причем не должны начинаться с цифры. Настоятельно не рекомендуется использование идентификаторов, совпадающих с ключевыми словами языка Java, что может привести к некорректной компиляции, либо интерпретации файлов классов JVM. Два специальных имени <init> и <clinit> также рассматриваются как идентификаторы.

Простейший пример описания класса, не имеющего полей и методов:


public abstract class some_package.SomeClass;

% это комментарий

extends

some_package.nested_package1.BaseClass;

implements % и это комментарий

some_package.Interface1, some_package.nested_package2.Interface2;


Описание поля имеет следующий вид:

[модификаторы_доступа] <имя_поля>:<тип_поля> [=<начальное значение>];


Здесь модификаторы_доступа - следующие слова: public, protected, private, final, static, transient, volatile, соответствующие флагам доступа поля ACC_PUBLIC, ACC_PROTECTED, ACC_PRIVATE, ACC_FINAL, ACC_STATIC, ACC_TRANSIENT, ACC_VOLATILE. Повторение одинаковых модификаторов доступа в объявлении одного поля и сочетания модификаторов, соответствующие запрещенным сочетаниям флагов доступа (см. The Java Virtual Machine Specification), вызывают ошибку времени компиляции. Поля интерфейса обязательно должны быть объявлены с модификаторами public, static и final. Имя_поля - корректный идентификатор. Тип_поля - имя класса либо имя примитивного типа (имена примитивных типов совпадают с соответствующими ключевыми словами языка Java - byte, short, int, long, char, float, double, boolean). Начальное значение может быть задано только для статического поля, если оно указано, то у поля создается атрибут ConstantValue. Начальное значение может быть целочисленной, вещественной, логической либо символьной константой для полей соответствующих типов. Вещественная константа может быть записана в десятичной либо экспоненциальной форме, в формате вещественных чисел, принятом в Java. Символьные константы заключаются в апострофы. Кроме того, может быть указан код символа как обычное целое число. Логические константы записываются в виде слов true и false. Примеры описаний полей:


public final static COUNT:int = 10;

static c:char = ‘A’;

static c1:char = 13;

private volatile m_flag:boolean;

protected m_list:java.util.ArrayList;


Описание метода в общем случае имеет вид:


[<модификаторы_доступа>] <имя_метода>(<тип_параметра_1>,<тип_параметра_2>, ... ,<тип_параметра_n>):<тип_возвращаемого_значения> [throws <класс_исключения_1>, ... , <класс_исключения_n>];

% для методов с модификатором abstract нижележащая часть описания

% отсутствует

maxstack <число>;

maxlocals <число>;

[<метка_1>:]

<команда_1>;

...

[<метка_n>:]

<команда_n>;

[

protected_blocks;

{<класс_исключения>|finally} <метка> : <метка> > <метка>;

...

{<класс_исключения>|finally} <метка> : <метка> > <метка>;

]

end;


Здесь модификаторы_доступа - ключевые слова: public, protected, private, static, final, abstract, соответствующие следующим флагам доступа метода: ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED, ACC_STATIC, ACC_FINAL, ACC_ABSTRACT. Повторение одинаковых модификаторов доступа в заголовке одного метода и сочетания модификаторов, соответствующие запрещенным сочетаниям флагов доступа (см. The Java Virtual Machine Specification), вызывают ошибку времени компиляции. Методы интерфейса обязательно должны быть объявлены с модификаторами public и abstract. Имя_метода - корректный идентификатор, либо <init> или <clinit> для конструкторов и статических инициализаторов. Типы параметров и тип возвращаемого значения должны быть именами классов, либо именами примитивных типов, принятыми в языке Java (byte, short, int, long, char, float, double, boolean). Кроме того, тип возвращаемого значения может быть указан как void. После ключевого слова throws в заголовке метода могут быть перечислены через запятую имена классов исключений, генерируемых методом. Для методов, не являющихся абстрактными, после заголовка обязательно записываются предложения maxstack и maxlocals, в которых указывается размер стека операндов и области локальных переменных метода (в четырехбайтных ячейках). Затем следует код метода в виде последовательности команд, разделенных точками с запятыми. Каждой команде может предшествовать метка, отделяемая от нее двоеточием. Метка должна быть корректным. Каждая команда может иметь не более одной метки, и каждая метка должна предшествовать той или иной команде. Однако, имеется специальная псевдокоманда none, для которой не генерируется какой-либо код (пустая команда). Ее можно использовать, если необходимо расположить более одной метки у одной команды или поместить метку в конец метода. После ключевого слова protected_blocks могут быть перечислены защищенные блоки (обработчики исключений) метода. Описание каждого защищенного блока состоит из имени класса исключения или ключевого слова finally и трех меток, разделенных символами ‘:’ и ‘>’. Первая из них указывает на начало защищенного блока, вторая на его конец, третья - на место в коде метода, куда переходит управление при возникновении исключения или при выходе из защищенного блока в случае finally.

Используемые в коде мнемонические имена команд совпадают с принятыми в The Java Virtual Machine Specification. Однако, как исключение, префикс wide не рассматривается как отдельная команда, вместо этого команды, его имеющие, записываются как wide_<имя_команды>. Форматы записи команд:

  • <мнемоническое_имя>; Такую форму имеют следующие команды: aaload, aastore, aconst_null, aload_0, aload_1, aload_2, aload_3 ,areturn, arraylength, astore_0, astore_1, astore_2, astore_3, athrow, baload, bastore, caload, castore, d2f, d2i, d2l, dadd, daload, dastore, dcmpg, dcmpl, dconst_0, dconst_1, ddiv, dload_0, dload_1, dload_2, dload_3, dmul, dneg, drem, dreturn, dstore_0, dstore_1, dstore_2, dstore_3, dsub, dup, dup2, dup2_x1, dup2_x2, dup_x1, dup_x2, f2d, f2i, f2l, fadd, faload, fastore, fcmpg, fcmpl, fconst_0, fconst_1, fconst_2, fdiv, fload_0, fload_1, fload_2, fload_3, fmul, fneg, frem, freturn, fstore_0, fstore_1, fstore_2, fstore_3, fsub, i2b, i2c, i2d, i2f, i2l, i2s, iadd, iaload, iand, iastore, iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5, iconst_m1, idiv, iload_0, iload_1, iload_2, iload_3, imul, ineg, ior, irem, ireturn, ishl, ishr, istore_0, istore_1, istore_2, istore_3, isub, iushr, ixor, l2d, l2f, l2i, ladd, laload, land, lastore, lcmp, lconst_0, lconst_1, ldiv, lload_0, lload_1, lload_2, lload_3, lmul, lneg, lor, lrem, lreturn, lshl, lshr, lstore_0, lstore_1, lstore_2, lstore_3, lsub, lushr, lxor, monitorenter, monitorexit, nop, pop, pop2, return, saload, sastore, swap;

  • <мнемоническое_имя> <метка>; Такую форму имеют команды перехода: goto, goto_w, if_acmpeq, if_acmpne, if_acmpge, if_acmpgt, if_icmple, if_icmplt, if_icmpne, ifeq, ifge, ifgt, ifle, iflt, ifne, ifnonull, ifnull, jsr, jsr_w;

  • <мнемоническое_имя> <целое число>; Число должно удовлетворять ограничениям конкретной команды: aload, astore, dload, dstore, fload, fstore, iload, istore, lload, lstore, ret, bipush, sipush, wide_aload, wide_astore, wide_dload, wide_dstore, wide_fload, wide_fstore, wide_iload, wide_istore, wide_lload, wide_lstore, wide_ret;

  • <мнемоническое_имя> {<полное_имя_класса>|@}::<имя_поля>:<тип_поля>; Тип_поля - имя примитивного типа, принятое в языке Java, либо имя класса. Команды: getfield, putfield, getstatic, putstatic;

  • <мнемоническое_имя> {<полное_имя_класса>|@}::<имя_метода>(<тип_параметра_1>, ... , <тип_параметра_n>):<тип_возвращаемого значения>; Здесь типы параметров и возвращаемого значения - имена примитивных типов, принятые в языке Java, имена классов, либо (только для возвращаемого значения) void. Команды: invokespecial, invokestatic, invokevirtual;

  • <мнемоническое_имя> <полное_имя_класса>; Такой формат имеют следующие команды: anewarray, checkcast, instanceof, new;

  • <мнемоническое_имя> <целое_число_индекс_переменной> <целое_число>; Команды: iinc, wide_iinc;

  • <мнемоническое_имя> <тип> <константа>; - команды ldc, ldc_w, ldc_2w. Здесь тип - int, float, string (для ldc, ldc_w), double, long (для ldc_2w). Константа должна иметь соответствующий тип (целые числа записываются обычным способом, вещественные - в десятичной или экспоненциальной форме, в формате, принятом в Java, строки записываются в двойных кавычках, при этом две двойные кавычки внутри строки интерпретируются как одна двойная кавычка в строке);

  • invokeinterface <имя_интерфейса>::<имя_метода>(<тип_аргумента_1>, ... , <тип_аргумента_2>):<тип_возвращаемого_значения> <целое_число>; - типы - аналогично другим командам вызова методов;

  • multianewarray <полное_имя_класса> <число_измерений>;

  • newarray {boolean|char|float|double|byte|short|int|long};

  • tableswitch <число_1>:<число_n> default: <метка_0> <число_1>:<метка_1> ... <число_n>:<метка_n>; Здесь числа число_1 ... число_n должны быть последовательными целыми числами. При этом числа, указанные сразу после мнемонического имени команды, должны совпадать с границами диапазона чисел, для которых указаны метки перехода. lookupswitch default:<метка_0> <число_1>:<метка_1> ... <число_n>:<метка_n>; Здесь среди чисел, для которых указаны метки перехода, не должно быть одинаковых. Эти числа должны быть целыми, они не обязаны быть упорядочены по возрастанию, сортировка происходит при обработке команды компилятором.


Тестовые примеры.


Для тестирования компилятора использовались, в частности, следующие примеры:


1.


%файл Summator.jsm

public class Summator;


fields;

private m_i:int;

methods;


%Конструктор. Заносит в поле m_i целое число, содержащееся в строке,

%передаваемой в качестве параметра. В случае, если строка не содержит

%правильной записи целого числа, либо это число отрицательное,

%то выводится сообщение об ошибке.

public <init>(java.lang.String):void;

maxstack 4;

maxlocals 2;

aload_0; %this

dup;

invokespecial java.lang.Object::<init>():void;

aload_1; %arg1

begin_try:

invokestatic java.lang.Integer::parseInt(java.lang.String):int;

dup;

iconst_0;

if_icmpge end_try;

new java.lang.Exception;

dup;

invokespecial java.lang.Exception::<init>():void;

athrow;

end_try:

putfield @::m_i:int;

return;

exception:

pop;

getstatic java.lang.System::out:java.io.PrintStream;

ldc string "Invalid argument";

invokevirtual java.io.PrintStream::println(java.lang.String):void;

return;

protected_blocks;

java.lang.Exception

begin_try : end_try > exception;

end;


%возвращает сумму натуральных чисел от 1 до m_i.

public getSum():int;

maxstack 3;

maxlocals 2;

iconst_0;

istore_1;

aload_0; %this

getfield @::m_i:int;

loop:

dup;

iload_1; %result

iadd;

istore_1; %result

iconst_1;

isub;

dup;

iconst_0;

if_icmpgt loop;

pop;

iload_1; %result

ireturn;

end;


%возвращает значение поля m_i

public getI():int;

maxstack 1;

maxlocals 1;

aload_0; %this

getfield @::m_i:int;

ireturn;

end;


2.


%файл Switches.jsm

public class Switches;


fields;


methods;


%оба метода функционально эквивалентны следующей функции, написанной на Java.

% static int function(int i) {

% switch(i) {

% case 1: return 2;

% case 2: return -1;

% default: return 0;

% }

% }


public static lookup(int):int;

maxstack 1;

maxlocals 1;

iload_0;

lookupswitch

default : l_def

1 : l_1

2 : l_2;

l_def:

iconst_0;

ireturn;

l_1:

iconst_2;

ireturn;

l_2:

iconst_m1;

ireturn;

end;

public static table(int):int;

maxstack 1;

maxlocals 1;

iload_0;

tableswitch 1:2

default : l_def

1 : l_1

2 : l_2;

l_def:

iconst_0;

ireturn;

l_1:

iconst_2;

ireturn;

l_2:

iconst_m1;

ireturn;

end;


3.

Следующий пример представляет собой программу, состоящую из 5 классов.


%-------------------------------------------------------------%

%файл Figure.jsm

public interface Figure;


methods;


public abstract getArea():double;

%-------------------------------------------------------------%


%-------------------------------------------------------------%

%файл Circle.jsm

public class Circle;


implements Figure;


fields;

private m_radius:double;


methods;


public <init>(double):void;

maxstack 4;

maxlocals 3;

aload_0;

invokespecial java.lang.Object::<init>():void;

dload_1;

dconst_0;

dcmpg;

ifge l_endif;

new java.lang.IllegalArgumentException;

dup;

invokespecial java.lang.IllegalArgumentException::<init>():void;

athrow;

l_endif:

aload_0;

dload_1;

putfield @::m_radius:double;

return;

end;


public getArea():double;

maxstack 4;

maxlocals 1;

aload_0;

getfield @::m_radius:double;

aload_0;

getfield @::m_radius:double;

dmul;

ldc2_w double 3.14159265;

dmul;

dreturn;

end;

%-------------------------------------------------------------%


%-------------------------------------------------------------%

%файл Rectangle.jsm

public class Rectangle;


implements Figure;


fields;

private m_a:double;

private m_b:double;


methods;


public <init>(double, double):void;

maxstack 4;

maxlocals 5;

aload_0;

invokespecial java.lang.Object::<init>():void;

dload_1;

dconst_0;

dcmpl;

iflt l_error;

dload_3;

dconst_0;

dcmpl;

ifge l_endif;

l_error:

new java.lang.IllegalArgumentException;

dup;

invokespecial java.lang.IllegalArgumentException::<init>():void;

athrow;

l_endif:

aload_0;

dload_1;

putfield @::m_a:double;

aload_0;

dload_3;

putfield @::m_b:double;

return;

end;


public getArea():double;

maxstack 4;

maxlocals 1;

aload_0;

getfield @::m_a:double;

aload_0;

getfield @::m_b:double;

dmul;

dreturn;

end;

%-------------------------------------------------------------%


%-------------------------------------------------------------%

%файл Square.jsm

public class Square;


extends Rectangle;


methods;


public <init>(double):void;

maxstack 5;

maxlocals 3;

aload_0;

dload_1;

dload_1;

invokespecial Rectangle::<init>(double, double):void;

return;

end;

%-------------------------------------------------------------%


%-------------------------------------------------------------%

%файл MainClass.jsm

public class MainClass;


methods;


public <init>():void;

maxstack 1;

maxlocals 1;

aload_0;

invokespecial java.lang.Object::<init>():void;

return;

end;


public static main(java.lang.String[]):void;

maxstack 8;

maxlocals 7;

iconst_3;

anewarray Figure;

astore_1;

aload_1;

iconst_0;

new Circle;

dup;

ldc2_w double 10;

invokespecial Circle::<init>(double):void;

aastore;

aload_1;

iconst_1;

new Rectangle;

dup;

dconst_1;

ldc2_w double 2;

invokespecial Rectangle::<init>(double, double):void;

aastore;

aload_1;

iconst_2;

new Square;

dup;

ldc2_w double 3;

invokespecial Square::<init>(double):void;

aastore;

dconst_0;

dstore_2;

iconst_0;

istore 4;

l_50:

iload 4;

aload_1;

arraylength;

if_icmpge l_75;

dload_2;

aload_1;

iload 4;

aaload;

invokeinterface Figure::getArea():double, 1;

dadd;

dstore_2;

iinc 4, 1;

goto l_50;

l_75:

new java.io.BufferedReader;

dup;

new java.io.InputStreamReader;

dup;

getstatic java.lang.System::in:java.io.InputStream;

invokespecial java.io.InputStreamReader::<init>(java.io.InputStream):void;

invokespecial java.io.BufferedReader::<init>(java.io.Reader):void;

astore 4;

l_50:

aload 4;

invokevirtual java.io.BufferedReader::readLine():java.lang.String;

invokestatic java.lang.Double::parseDouble(java.lang.String):double;

dstore 5;

getstatic java.lang.System::out:java.io.PrintStream;

dload 5;

dload_2;

dadd;

invokevirtual java.io.PrintStream::println(double):void;

l_114:

goto l_127;

l_117:

astore 4;

getstatic java.lang.System::out:java.io.PrintStream;

ldc string "Error";

invokevirtual java.io.PrintStream::println(java.lang.String):void;

l_127:

return;

protected_blocks;

java.io.IOException l_75 : l_114 > l_117;

end;

%-------------------------------------------------------------%


Данная программа функционально эквивалентна следующему коду на Java (ассемблерный вариант создан на основе дизассемблированной с помощью утилиты javap Java-программы):


//-----------------------------------------------------------//

public interface Figure {

double getArea();

}

//-----------------------------------------------------------//


//-----------------------------------------------------------//

public class Circle implements Figure {

private double m_radius;


public Circle(double radius) {

if(radius<0)

throw new IllegalArgumentException();

m_radius = radius;

}


public double getArea() {

return m_radius*m_radius*Math.PI;

}

}

//-----------------------------------------------------------//


//-----------------------------------------------------------//

public class Rectangle implements Figure {

private double m_a;

private double m_b;


public Rectangle(double a, double b) {

if(!((a>=0)&&(b>=0)))

throw new IllegalArgumentException();

m_a = a;

m_b = b;

}


public double getArea() {

return m_a*m_b;

}

}

//-----------------------------------------------------------//


//-----------------------------------------------------------//

public class Square extends Rectangle {

public Square(double a) {

super(a, a);

}

}

//-----------------------------------------------------------//


//-----------------------------------------------------------//

import java.io.*;


public class MainClass {

public static void main(String[] args) {

Figure[] figures = new Figure[3];

figures[0] = new Circle(10);

figures[1] = new Rectangle(1, 2);

figures[2] = new Square(3);

double sum = 0;

for(int i = 0; i<figures.length; i++)

sum += figures[i].getArea();

try{

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

double d = Double.parseDouble(br.readLine());

System.out.println(d+sum);

} catch(IOException exc) {

System.out.println("Error!");

}

}

}

//-----------------------------------------------------------//

Проектирование и реализация компилятора.


Для реализации компилятора был использован язык программирования Java (JDK версии 1.5). Это позволяет запускать данный компилятор на любой платформе, для которой существует виртуальная машина Java v 1.5.

При каждом запуске компилятора обрабатывается один файл исходного текста на языке ассемблера для платформы Java. Компилятор принимает два аргумента командной строки: имя файла исходного текста и имя создаваемого файла класса (явное указание расширения .class обязательно). В случае, если выходной файл уже существует, он перезаписывается без предупреждения. В случае синтаксической или иной ошибки на консоль выводится соответствующее сообщение.

Можно выделить несколько основных этапов компиляции (проходов):

  • Чтение исходного файла. При этом он разбивается на предложения, разделенные точками с запятыми, также выбрасываются комментарии;

  • Разбор исходного текста. При последовательном переборе списка предложений выделяются синтаксические конструкции. При разборе используется лексический анализатор, разделяющий предложения на лексемы. На основании выделенных синтаксических конструкций генерируется внутреннее представление программы, имеющее вид древовидной структуры данных, корнем которой является представление класса в целом, узлами первого уровня - объекты, соответствующие методам, полям, элементам Constant Pool и т. д.;

  • Замена номеров меток соответствующими смещениями в коде методов;

  • Генерация байт-кода методов как массивов байт;

  • Генерация файла класса на основании внутреннего представления программы.

Данное деление является условным и не означает строгой временной последовательности. Третий и четвертый этапы, по сути дела, являются частями второго этапа.

Диаграмма пакетов проекта изображена на рис. 1.



Рис. 1.

Диаграмма пакетов


Корневой пакет проекта имеет имя jasm. Он содержит класс MainClass, метод main() которого является точкой входа в программу, и классы SyntaxErrorException и InternalCompilerErrorException, унаследованные от java.lang.Exception и представляющие ошибки, которые могут возникнуть в процессе компиляции. Пакет compiler содержит классы, ответственные за разбор исходного текста, причем классы, работающие с кодом методов, содержатся во вложенном пакете code_compiler. Пакет structures содержит классы, из объектов которых состоит внутреннее промежуточное представление программы, а также некоторые вспомогательные классы. Он имеет три вложенных пакета: commands, consts и attributes, классы из которых описывают, соответственно, команды байт-кода, элементы Constant Pool и атрибуты полей и методов. В пакет commands в свою очередь вложены пакет command_formats, содержащий базовые абстрактные классы для команд типичных форматов, пакеты, содержащие классы, представляющие команды каждого из типичных форматов, а также пакет special для классов, представляющих команды, имеющие особый формат.

Большинство классов из пакета structures входят в иерархию, корнем которой является интерфейс IStructure, содержащий два метода int getLength() и byte[] getContent(), позволяющие получить, соответственно, размер, который займет структура при записи в выходной файл, и массив байт, которыми она представляется в выходном файле. Данный интерфейс не используется для полиморфного вызова методов, он играет лишь роль структурирования программы. Основные классы пакета structures изображены на диаграмме на рис. 2.


Рис. 2.

Классы пакета jasm.structures.

Генерируемый класс как целое представляется объектом класса ClassFile, который содержит в своих полях ссылки на объекты классов ConstantPool, FiledInfo и MethodInfo, описывающие область констант, поля и методы создаваемого класса. Сам класс ClassFile интерфейс IStructure не реализует. Среди его членов следует отметить метод writeToFile(), создающий файл класса на основании информации, содержащейся в объекте.

Данный компилятор может создавать атрибуты методов Code, Exceptions и атрибут поля ConstantValue, которые представляются классами, производными от AttributeStructure. Отмечу, что объект класса CodeAttribute содержит байт-код метода уже в виде массива байт, а не в виде объектов классов, представляющих отдельные команды (производных от Command), списки таких объектов используются лишь в процессе обработки кода метода.

Абстрактный класс Command дополнительно к методам интерфейса IStructure содержит абстрактный метод byte getByte(int n), который должен возвращать байт с заданным номером в команде. Еще один метод changeOffset имеет пустую реализацию, но переопределяется в классах-потомках, соответствующих командам перехода. Он используется для замены номеров меток смещениями на третьем этапе компиляции. Непосредственными наследниками класса Command являются классы, соответствующие типичным форматам команд (абстрактные) и командам, имеющим уникальные форматы. Большинство команд представляются классами, наследующими классы типичных форматов. Имена классов команд имеют вид C_xxx, где xxx - мнемоническое имя команды. Пустой команде none соответствует класс NoCommand.

Класс ConstantPool содержит как общий список для всех типов констант, хранящий объекты класса CpInfo (базовый тип для классов, представляющих различные виды констант), так и списки для констант отдельных типов, содержащие индексы элементов в первом списке. Эти списки описываются классами, вложенными в класс ConstantPool. Такая структура используется для того, чтобы при добавлении константы можно было быстро проверить, не присутствует ли уже идентичный элемент в ConstantPool (эта проверка производится не для всех типов констант). Для каждого типа констант в классе ConstantPool существует свой метод добавления, который возвращает индекс добавленного (или найденного существующего) элемента в общем списке. Среди наследников CpInfo имеется специальный класс CpNone, который соответствует пустой структуре, вставляемой после констант типа Long и Double т. к. следующий за ними индекс считается неиспользуемым.

За процесс компиляции отвечает пакет compiler, который содержит следующие классы:

  • Source - выполняет функцию выделения предложений в исходном тексте. Конструктор этого класса принимает в качестве параметра имя файла, содержимое которого разбивается на предложения и заносится в коллекцию типа ArrayList<String>. Метод String nextStatement() при каждом вызове возвращает очередное предложение;

  • StringParser - выполняет функцию разделения строк на лексемы. Каждый объект этого класса соответствует одной обрабатываемой строке.

  • TokenRecognizer - имеет статические методы, позволяющие определить, является ли некоторая строка ключевым словом или корректным идентификатором. Объекты этого класса никогда не создаются;

  • DescriptorCreator - содержит статические методы для создания дескрипторов типов, полей и методов из строк, содержащих запись типов и сигнатур, используемую в языке. Экземпляры класса также не создаются;

  • ClassHeaderParser, FieldDeclarationParser, MethodHeaderParser - используются при анализе заголовка класса, описаний полей и заголовков методов. Объекты этих классов сохраняют в себе информацию, извлеченную из анализируемого в конструкторе класса предложения;

  • code_compiler.CodeCompiler - осуществляет анализ кода метода и генерацию байт-кода (включая третий и четвертый этапы компиляции). Данный процесс будет рассмотрен подробнее ниже;

  • code_compiler.CommandCompiler - анализирует команды в исходном коде метода и создает объекты классов-потомков Command;

  • code_compiler.Label - представляет метку в коде метода;

  • code_compiler.LabelTable - таблица меток метода. Содержит имена меток, номера соответствующих им строк и смещения команд.

  • SourceAnalyser - занимает центральное место в процессе анализа исходного текста. Конструктор данного класса принимает в качестве параметра объект класса Source. При вызове метода analyse() происходит анализ исходного кода и генерируется промежуточное представление программы в виде описанной выше структуры. В процессе анализа используются классы StringParser, ClassHeaderParser, FieldDeclarationParser, MethodHeaderParser, CodeCompiler и др. Данный метод возвращает объект класса ClassFile.

Класс MainClass содержит единственный метод main, являющийся точкой входа в программу. Здесь вначале создается объект класса Source, который передается для обработки объекту класса SourceAnalyser, затем у возвращенного методом SourceAnalyser.analyse() объекта класса ClassFile вызывается метод writeToFile, который и генерирует файл класса, являющийся результатом работы компилятора. Все перечисленные операции заключены в блок try/catch, перехватывающий любые исключения, в случае возникновения которых на консоль выводится соответствующее сообщение и процесс компиляции завершается. Диаграмма, в упрощенном виде показывающая этот процесс, изображена на рис. 3.


Рис. 3.

Обработка исходного файла.

Рассмотрим подробнее процесс компиляции кода метода. После обработки заголовка метода с помощью класса MethodHeaderParser, в случае, если метод не является абстрактным, в методе SourceAnalyser .analyse() считываются предложения maxstack и maxlocals. Затем считываются и заносятся в массивы предложения, содержащие команды и описания защищенных блоков. Эти массивы, а также ссылка на объект ConstantPool, представляющий область констант класса, передаются в качестве параметров конструктору класса CodeCompiler. У созданного объекта CodeCompiler вызывается метод compile(), возвращающий объект класса CodeAttribute, описывающий атрибут Code, содержащий байт-код метода. При этом происходят следующие процессы. В конструкторе класса CodeCompiler из строк, содержащих команды, выделяются имена меток, которые сохраняются в объекте класса LabelTable. Затем обрабатывается список строк, описывающих защищенные блоки. В методе CodeCompiler.compile() выполняются следующие операции. Сначала с помощью объекта класса CommandCompiler для каждой команды создается объект соответствующего класса. При этом одновременно для команд, при которых имеется метка, в объекте LabelTable сохраняется информация о смещении метки относительно начала метода. Как в описаниях защищенных блоков, так и в объектах, соответствующих командам перехода, на момент окончания этого шага вместо смещений перехода, содержатся порядковые номера команд, при которых расположены соответствующие метки. Замена их на действительные смещения производится на последнем шаге с помощью методов LabelTable.changePC() и Command.changeOffset().

Заключение.


Технология Java ориентирована на использование одного языка программирования. Система типов данных и другие особенности языка Java тесно связаны с функционированием JVM и форматом файла класса. Однако, существует открытая спецификация, позволяющие создавать как собственные реализации JVM, так и альтернативные средства разработки. С ее использованием мною разработан язык JASM, представляющий собой язык ассемблера для платформы Java, который позволяет создавать файлы классов, использующие значительную часть возможностей JVM, и реализован его компилятор.

Использованная литература.


  1. Грис, Д. Конструирование компиляторов для цифровых вычислительных машин. М., «Мир», 1975.

  2. Эккель, Б. Философия JAVA. СПб. 3-е изд.: Питер, 2003.

  3. Tim Lindholm, Frank Yellin. The Java Virtual Machine Specification Second Edition. Sun Microsystems Inc. 1999.

Рефетека ру refoteka@gmail.com