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

Вид материалаПрограмма

Содержание


Промышленные виртуальные машины
Java Virtual Machine
Common Language Runtime
Подобный материал:
1   2   3   4   5

Промышленные виртуальные машины


В качестве одного из первых примеров абстрактной машины можно назвать OCODE, которая была разработана для реализации мобильного компилятора языка BCPL [_]. Самой известной абстрактной машиной была P-машина1, которая была разработана при реализации первых промышленных компиляторов языка Pascal и использовалась в системе UCSD-Pascal [_]. Pascal-программы транслировались в P код (P-code) и затем выполнялись программой-интерпретатором.

Нужно сказать, что все реализация всех языков программирования, разработанных Н.Виртом и его командой, основывались на той или иной абстрактной машине. Например, компилятор Modula-2 транслировал исходный код в M-код, являющийся развитием P-кода. Для реализации Оберона ученик Вирта М.Франц [_] разработал технологию генерации кода «на лету» (code generation on the fly), основанную на специальном представлении программы в виде семантического словаря (Semantic Dictionary Encoding, SDE). Компилятор Оберона транслировал исходную программу в код SDE, который можно либо выполнять непосредственно, либо «на лету» переводить в коды целевого компьютера. Одной из систем, которая использует SDE с небольшими модификациями, является система BlackBox Component Builder [_], разработанная фирмой Oberon Microsystems, которую основали Н.Вирт и его коллеги и ученики.

Фирма Microsoft использовала варианты P-кода в ранних версиях системы Visual Basic. Прямой наследницей P-машины является виртуальная машина Java (Java Virtual Machine, JVM). Применение виртуальной машины в этом случае обеспечило мобильность программ, реализованных на языке программирования Java.

Фирма Microsoft применила идею виртуальной машины при разработке своей интегрированной среды Visual Studio.NET [_]. Хотя Microsoft предпочитает называть эту виртуальную машину «общеязыковой исполняющей средой» (Common Language Runtime, CLR), тем не менее, это настоящая виртуальная машина. В данном случае эта абстрактная машина больше обеспечивает не переносимость программ, а их совместимость в рамках одной среды. Отдельные компоненты большой программы можно разрабатывать на разных языках, входящих в состав Visual Studio.NET, а затем собрать в единую систему.

Еще одной интересной разработкой, в основе которой лежит абстрактная машина, является системы Forth [_].

Интересно, что все промышленные виртуальные машины имеют стековую ROSC-архитектуру. Это связано с тем, что такая архитектура наиболее приспособлена для языков высокого уровня. Рассмотрим более подробно некоторые «промышленные» виртуальные машины. Начнем с абстрактной P-машины, поскольку она послужила прототипом и для JVM, и для CLR.

P-машина


В системе UCSD-Pascal результатом трансляции2 исходной Pascal-программы является программа на P-коде, которая состоит из двух компонент: массив кодов и структура, представляющая модель памяти (рис. 1.1) для данной Р-программы.



Рис.1.1. Структура памяти Р-программы

Модель памяти содержит статическую часть, где записаны константы, и динамическую часть, где размещаются стек и куча. Структура динамической памяти представлена на рис. 1.2. Для работы со стеком и кучей процессор P-машины содержит несколько регистров, в которых записаны адреса динамической памяти:
  • PC — счетчик адреса команд;
  • SP — указатель стека процедуры;
  • MP — указатель начала стекового фрейма;
  • NP — указатель кучи;
  • EP — указатель границы общего стека в динамической памяти.



Рис. 1.2. Структура динамической памяти Р-машины

Для каждого вызова процедуры (в том числе и рекурсивного) в общем стеке выделяется стековый фрейм, структура которого показана на рис. 1.3. В этом фрейме, начало которого отмечает указатель MP, выделяется место для локального стека, вершину которого определяет указатель SP. Другой конец фрейма записан в регистре EP. Указатель MP отмечает начало служебной части фиксированного размера, с помощью которой обеспечиваются связи с вызывающей процедурой:
  • static link обеспечивает доступ данной процедуры к переменным объемлющей процедуры3;
  • dynamic link — это связь с фреймом стека вызывающей процедуры; содержит значение MP вызывающей процедуры;
  • previous EP хранит значение EP вызывающей процедуры;

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



Рис.1.3. Фрейм процедуры в общем стеке.

В листинге 1.1 показана простая программа на языке Pascal, состав стека P-машины для которой можно видеть на рис. 1.4.

Листинг 1.1. Программа на языке Pascal

{ главная программа }

program main (input, output);

var j: integer;

{ вложенная процедура P }

procedure P;

var i: integer;

{ вложенная процедура Q }

procedure Q;

begin

if i > 0 then begin

i := i - 1; j := j + 1;

Q { рекурсивный вызов процедуры Q }

end

else write(input, 0);

end;

{ тело процедуры P }

begin

i := 2;

Q; { вызов процедуры Q }

end;

{ тело главной программы }

begin

j := 0;

P; { вызов процедуры P }

end.



Рис. 1.4. Стек программы из листинга 1.1.

На рисунке можно видеть, что процедура Q вызвана рекурсивно 3 раза, и для каждого вызова создан фрейм стека, который связан динамическими связями с предыдущим вызовом. Статические связи показывают текстуальную вложенность процедур.

Р-машина оперирует данными различных типов: целыми и вещественными числами, булевскими величинами, множествами, символами, адресами. Фактически типы данных P-машины совпадают с типами данных языка Pascal с учетом конкретной платформы. Р-машина является стековой машиной. В систему команд входят всего 60 операций, большинство из которых являются стековыми: операнды извлекаются из стека и результат помещается в стек. Команды имеют следующие форматы:
  • КОП P Q
  • КОП P
  • КОП Q
  • КОП

КОП — это код операции. Аргумент P используется для определения уровня статического блока, а Q, как правило, определяет смещение во фрейме. Иногда Q представляет собой непосредственный операнд-константу.

Команды без параметров обычно применяются к одному или двум элементам стека, и результат помещают снова в стек. В качестве примеров можно привести следующие операции:
  • ADR — сложение вещественных; извлекает два верхних элемента стека, которые являются вещественными числами, выполняет сложение и помещает результат в стек;
  • INN — проверка принадлежности множеству; извлекает из стека два верхних элемента, которые представляют собой множество и элемент; выполняется проверка принадлежности элемента множеству и на вершину стека помещается булевское значение;
  • FLT — преобразовать целое на вершине стека в вещественное;

Команды загрузки-сохранения и пересылки содержат, как правило, один или два аргумента, например:
  • LDCI 4 — загрузка целой константы 4 в стек;
  • LODI 0 5 — загружает в стек целое значение из слов с адресом (0,5);
  • LDA 0 6 — загрузка в стек адреса (0,6);
  • STRI 1 5 — сохранить значение с вершины стека в слове с адресом (1,5);
  • MOV 10 — извлечь из стека исходный и целевой адрес и переслать 10 слов, начиная с исходного адреса по целевому адресу;

В состав системы команд входит несколько команд перехода, например:
  • UJP L2 — безусловный переход по метке L2;
  • FJP L5 — условный переход по метке L5, если на вершине стека false.

Метки располагаются непосредственно в кодовой части среди команд. Эти команды используются для организации циклов в Р-программе. Например, оператор цикла

while (выражение) оператор

представляется следующей схемой в последовательности команд в массиве кодов:

L1

код для вычисления выражения и помещения значения на вершину стека

FJP L2

код для выполнения оператора

UJP L1

L2

В систему команд входят команды вызова стандартных и пользовательских процедур. Требуемые параметры, естественно, должны быть предварительно помещены в стек. Например, команда CSP COS означает, что требуется вызвать стандартную функцию косинуса, которая извлекает аргумент с вершины стека и результат помещает туда же.

Система команд P-машины включает 4 команды для организации вызова пользовательских процедур: MST, CUP, ENT и RET. Каждая пользовательская процедура начинается с двух команд ENT, которые устанавливают EP и SP для данного вызова процедуры. Возврат из процедуры осуществляется командой RET, которая освобождает стек. Вызов пользовательской процедуры осуществляется по следующей схеме:

MST P

код для помещения параметров в стек

CUP P Q

Операнд P определяет уровень вложенности вызова процедуры. Команды MST и CUP формируют фиксированную часть фрейма стека, устанавливая MP, размещая там связи и адрес возврата.

Программу на P-коде можно в дальнейшем откомпилировать в машинный код конкретной машины, однако это обычно не делают. Программа на Р-коде выполняет интерпретатор. В конце 70-х годов использование Р-машины способствовало широкому распространению языка Pascal. При наличии компилятора, написанного на Pascal, для его использования в новой среде требовалось реализовать только интерпретатор P-кода для этой среды, на что уходило около месяца работы.

Java Virtual Machine


Виртуальная Java-машина4 (Java Virtual Machine, JVM) является значительно более сложной по сравнению с P-машиной. Основными целями при ее разработке были эффективность, защищенность и переносимость. Программы представляются для JVM в байт-коде и являются результатом трансляции программ на языке Java [_], разработанном в фирме Sun Microsystems Inc. Программа на Java представляет собой множество классов. Результатом трансляции каждого исходного класса является отдельный класс-файл. Основными компонентами JVM являются:
  • Загрузчик классов, который загружает, связывает и инициализирует классы;
  • Исполнитель (интерпретатор), который и выполняет программу в байт-кодах;
  • Интерфейс потоков, управляющий параллельной работой;
  • Модуль управления памятью, который управляет кучей, где хранятся объекты и массивы;
  • Модуль управления обработкой исключительных ситуаций, систематически отслеживающий возникающие в процессе выполнения исключительные ситуации и ошибки;
  • Модуль управления защитой, который препятствует запуску «враждебных» программ.

Каждый класс-файл содержит байт-код всех описанных методов, таблицу имен, таблица связей с суперклассами и т.д. Вся эта информация записана в единственной структуре5 ClassFile следующего вида:

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];

};

Типы u2 и u4 — это беззнаковые целые размером 2 и 4 байта соответственно. Остальные типы определены в составе Java-машины. Например, тип field_info представляется структурой со следующими полями:

field_info

{    u2 access_flags;

     u2 name_index;

     u2 descriptor_index;

     u2 attributes_count;

     attribute_info attributes[attributes_count];

};

Названия полей всех структур говорят сами за себя. Например, поле access_flags в структуре ClassFile определяет, как был объявлен данный класс: public, final, abstract или interface. В структуре field_info поле с таким же названием, очевидно, определяет модификатор доступа к данному полю: public, protected, private, static, final. В структуре ClassFile все поля с суффиксом _count представляют собой размеры (количество элементов) массивов соответствующего типа, представленных в этой же структуре.

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

Верификация обходится недешево, но позволяет избавиться от трудоемкой проверки в процессе выполнения. Тем самым повышается скорость выполнения программы.

Типы данных Java-машины делятся на два вида: простые (primitive) и ссылочные (reference). Аналогичное деление существует в языке Java, так что JVM практически непосредственно отображает типы Java. Значения простых типов — это целые и вещественные числа, которые определены в Java. Типа boolean, который определен в языке Java, виртуальная Java-машина не поддерживает: значения этого типа представляются как целые 0 (false) и 1 (true). Символьный тип char тоже представляется целыми. В JVM используется один простой тип returnAddress, который не определен в языке Java. Название «говорящее»: значением является адрес возврата в вызывающую функцию. Значения простых типов размещаются в стеке.

К ссылочным типам относятся типы классов, типы массивов и типы интерфейсов. Однако в спецификации сказано, что объект — это есть экземпляр класса или массива, объектов типа интерфейса не существует. Объекты создаются в куче, а доступ к ним осуществляется с помощью значений ссылочного типа, которые являются указателями. Специальным значением ссылочного типа является null.

Виртуальная Java-машина поддерживает параллельные процессы, поэтому память периода выполнения устроена значительно сложнее, чем в P-машине. Для каждого потока (thread) JVM поддерживает регистр pc – счетчик адреса команды. Для каждого потока организуется свой стек. При каждом вызове метода в стеке выделяется фрейм, в котором содержатся локальные переменные и динамические связи (подобные тем, которые изображены на рис. 1.3 и 1.4). А вот куча — одна общая для всех потоков. Сама программа размещается в области методов (method area). Собственно, это и есть кодовый сегмент, аналогичный кодовому сегменту P-машины. С каждой областью методов связан пул констант.

Состав системы команд JVM тоже значительно шире, чем система команд P-машины и включает более 160 команд, которые в спецификации JVM объединяются в следующие группы:
  • загрузка-сохранение;
  • арифметические и битовые команды;
  • преобразование типов;
  • оперирование объектами и массивами;
  • работа со стеком;
  • передача управления;
  • вызов методов;
  • обработка исключительных ситуаций и параллельная работа.

Архитектура JVM стековая, поэтому большинство команд безоперандные, либо имеют один-два операнда, как команды P-машины. Многие команды отличаются только типами операндов. Например, в состав арифметических команд входит 4 команды сложения: iadd, ladd, fadd, dadd. Вообще, состав системы команд производит впечатление несколько хаотичной разработки. Например, реализовано семь команд для загрузки целых констант в стек, но только две — для загрузки длинных (long) целых констант; три команды загрузки коротких вещественных (float) и две — для загрузки длинных вещественных (double).

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

double i = 0.0;

while (i < 100.1)

i++;

Эти операторы компилируются в следующую последовательность байт-кодов:

0 dconst_0

1 dstore_1

2 goto 9

5 dload_1

6 dconst_1

7 dadd

8 dstore_1

9 dload_1

10 ldc2_w #4

13 dcmpg

14 iflt 5

Первые три команды реализуют инициализацию переменной i, следующие четыре команды — операция инкремента переменной i. И наконец, последние четыре — проверку выражения в условии оператора while. Числа слева — это смещение в байтах соответствующей команды. Эти смещения задаются в командах перехода в качестве операндов: в команде безусловного перехода goto и в команде условного перехода iflt.

Тот же цикл с целой переменной i:

int i = 0;

while (i < 100)

i++;

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

0 iconst_0

1 istore_1

2 goto 8

5 iinc 1 1

8 iload_1

9 bipush 100

11 if_icmplt 5

Инициализация переменной тоже выполняется тремя командами, а вот инкремент осуществляется одной командой. Последние три команды — это проверка значения i и условный переход.

Common Language Runtime