Глава 1. Системное программное обеспечение и архитектура ЭВМ

Системное программное обеспечение, будь то операционная система, ассемблер или компилятор для какого-нибудь языка высокого уровня, всегда создается для конкретной вычислительной машины. Машинная зависимость является одной из характерных особенностей, которая во многом отличает системное ПО от прикладного. При разработке прикладной программы основное внимание сосредоточено на предметной стороне дела, и особенности конкретного компьютера мало влияют на программу — ЭВМ используется только как инструмент для решения задачи. С другой стороны, операционная система обычно реализуется для конкретной вычислительной системы, и на другом компьютере работать не будет. А инструментальное программное обеспечение по сути своей предназначено для создания программ на конкретную машину. В этом смысле системные программы практически всегда зависят от компьютера, поэтому системный программист должен прекрасно разбираться в устройстве вычислительных машин.

ЭВМ можно изучать под разным углом зрения. Например, можно рассматривать компьютер как набор связанных функциональных блоков различного назначения. С другой стороны, можно изучать множество логических элементов, и как они используются при построении функциональных блоков. Инженеров-электронщиков, очевидно, интересуют электрические схемы и прохождение сигналов. Такие описания представляют собой различные модели компьютера, предназначенные для изучения их человеком. Вместо слова «модель» часто употребляется термин «спецификация» или «архитектура».

НЕ нашли? Не то? Что вы ищете?

Программисты используют совсем другую модель. Модель компьютера для программиста называется архитектурой команд [_] или просто архитектурой.

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

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

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

q  типы данных, которыми способен оперировать процессор;

q  организация памяти и состав регистров процессора;

q  набор команд и способы адресации аргументов.

При реализации операционной системы обязательно рассматривать еще организацию системы прерываний и системы ввода-вывода.

С другой стороны, системное программное обеспечение имеет ряд аспектов, которые совершенно не зависят от типа вычислительной системы. Например, архитектура ЭВМ никак не влияет на синтаксический анализ программы, выполняемый любым компилятором. На работе мейкера тоже никак не отражаются ни организация памяти, ни набор регистров. В трансляторе с ассемблера не зависит от архитектуры команд «сбор» меток в таблицу имен. Даже в операционной системе есть машинно-независимые компоненты, например, командный процессор.

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

Реальные и виртуальные компьютеры

В настоящее время различают несколько различных типов архитектур [_]:

q  архитектура с полным набором команд (Complex Instruction Set Computer, CISC);

q  архитектура с сокращенным набором команд (Reduced Instruction Set Computer, RISC);

q  архитектура с безоперандным набором команд (Removed Operand Set Computer, ROSC).

В наши дни наиболее распространена архитектура CISC. Характерными чертами такой архитектуры являются:

q  небольшое число регистров общего назначения;

q  большое количество машинных команд, некоторые из которых весьма сложны;

q  разнообразие способов адресации операндов;

q  множество форматов команд различной длины.

Типичным представителем компьютеров с такой архитектурой является Pentium [_].

Архитектура с сокращенным набором команд обладает прямо противоположными характеристиками:

q  большое количество регистров общего назначения;

q  относительно небольшое множество простых машинных команд;

q  количество способов адресации ограничено; фактически все команды, кроме команд загрузки и сохранения, выполняют операции на регистрах;

q  команды имеют одинаковую длину, и количество форматов команд ограничено.

Все это позволило существенно упростить аппаратуру и значительно повысить быстродействие. Как правило, такая архитектура используется в суперЭВМ вроде Cray. Однако и микропроцессоры «доросли» до RISC.

Архитектура с безоперандным набором команд основана на применении стека. Появление ROSC-архитектуры вызвано стремлением сократить семантический разрыв [_], существующий между языками высокого уровня и аппаратурой компьютера. Стеки широко используются в компиляторах для реализации языков высокого уровня. И в оттранслированной программе стеки используется тоже очень широко. Например, вложенные или рекурсивные вызовы подпрограмм, реализация локальной области видимости, вычисление арифметических выражений проще всего выполнить с помощью стека.

В настоящее время можно наблюдать тенденцию сближения архитектур. Например, в микропроцессоре Pentium мы можем обнаружить элементы и RISC-архитектуры, и ROSC-архитектуры [_]. Однако большинство реальных ЭВМ по тем или иным причинам часто обладают нестандартными или даже уникальными особенностями. В том же Pentium многочисленные методы адресации разрешается задавать только для одного из операндов, а второй непременно должен быть в регистре. Кроме того, регистры (базовый и индексный), используемые для формирования адреса операнда, задаются неявно с помощью комбинации 5 битов в специальном байте modR/M в составе команды. Мало того, в формировании физического адреса помимо аргументов в команде неявно участвует один из сегментных регистров. Обычно он выбирается процессором по умолчанию в зависимости от команды и метода адресации, однако его можно назначить принудительно с помощью префикса замены сегмента.

Такие особенности фактически не имеют отношения к принципам организации системного ПО, но сильно усложняют машинно-зависимую часть, например формирование двоичных команд в трансляторе с ассемблера. Поэтому мы будем использовать абстрактную учебную машину VM (Virtual Machine). Это позволит нам четко отделить теоретические аспекты и основные концепции от деталей реализации, связанные с архитектурой команд.

Примечание

В настоящее время абстрактную машину обычно называют виртуальной. Иногда используется термин «псевдомашина». Мы будем использовать эти термины как эквивалентные.

Эта книга не первая, в которой для изучения программирования используется абстрактная учебная машина. Например, Л. Бек в книге [_] по системному программированию использовал две разных учебных машины. Один из «гуру» программирования Никлаус Вирт в книге [_] при реализации компилятора для учебного языка тоже описал и использовал виртуальный компьютер. В другой своей книге о компиляторах [_] профессор Н. Вирт описывает виртуальный RISC-компьютер, в коды которого выполняется трансляция языка Оберон-0. В книге [_] при описании процесса трансляции с языка высокого уровня тоже использована простая виртуальная машина. В книге [_] авторы учебного курса «Введение в компьютерные системы», преподаваемого более чем в 90 университетах мира, описывают архитектуру учебной виртуальной машины Y86. Однако самым знаменитым абстрактным компьютером является учебная машина MIX, которую описал Дональд Кнут в фундаментальном труде «Искусство программирования» [_].

Кнут в настоящее время пишет четвертый и пятый том «Искусства программирования» (книга уже издается по частям). В связи с тем, что современные компьютеры довольно сильно отличаются от MIX, он разработал новую виртуальную машину MMIX (Modification MIX). В книге [_], где описана архитектура этого виртуального компьютера, профессор Д. Кнут написал:

MMIX — это RISC-компьютер, разработанный автором для иллюстрации программирования на машинном уровне. В следующих изданиях «Искусства программирования» MMIX заменит машину 60-х MIX.

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

Виртуальные машины и промышленное программирование

Виртуальные машины находят широкое применение не только в обучении, но и в практике реального промышленного программирования. Одной из основных проблем в системном программировании является проблема переносимости (мобильности) программного обеспечения.

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

Естественно, переносить имеет смысл только «многоразовое» ПО. Проблема мобильности программного обеспечения изучалась еще в 60-е годы прошлого столетия в основном на опыте переноса компиляторов. Именно в том время родилась идея использования абстрактной машины для облегчения переноса программ. Рассмотрим суть идеи. Пусть требуется реализовать N языков программирования на M различных архитектур. Очевидно, потребуется реализовать N*M компиляторов.

Пусть теперь определена подходящая виртуальная машина, и исходную программу можно транслировать в коды виртуальной машины. Тогда требуется разработать только N компиляторов, осуществляющих перевод исходной программы в коды виртуальной машины, и для каждого из М целевых компьютеров потребуется реализовать «исполнитель» команд виртуальной машины.

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

Во-первых, использование виртуальной машины в значительной степени повышает переносимость: оттранслированная программа независима от исполняющей платформы и будет работать на любой ЭВМ, где реализован соответствующий исполнитель. С другой стороны, и компилятор, реализующий перевод исходной программы в коды виртуальной машины, становится менее зависимым от целевой платформы.

Во-вторых, налицо существенное сокращение объема работ: вместо N*M программ требуется только N+M программ. Поскольку виртуальную машину можно сделать настолько удобной, насколько возможно, — это обычно позволяет существенно уменьшить сложность реализации компилятора. Реализовать же исполнитель можно самыми разными способами:

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

q  можно реализовать транслятор из кодов виртуальной машины в коды целевой машины (генератор кода);

q  еще один способ – реализовать транслятор с ассемблера виртуальной машины в ассемблер исполняющего компьютера.

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

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

При сочетании первого и второго подхода получаем еще один вариант реализации исполнителя: JIT-компилятор.

Для реализации третьего подхода требуется разработка ассемблера виртуальной машины. Сам же транслятор можно реализовать несколькими способами: можно реализовать прямой транслятор; можно использовать макроассемблер целевой машины, определив для каждого оператора ассемблера виртуальной машины соответствующий макрос.

Таким образом, использование виртуальной машины обеспечивает беспрецедентную гибкость при разработке системного ПО.

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

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

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

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

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

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

P-машина

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

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

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

q  PC — счетчик адреса команд;

q  SP — указатель стека процедуры;

q  MP — указатель начала стекового фрейма;

q  NP — указатель кучи;

q  EP — указатель границы общего стека в динамической памяти.

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

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

q  static link обеспечивает доступ данной процедуры к переменным объемлющей процедуры[3];

q  dynamic link — это связь с фреймом стека вызывающей процедуры; содержит значение MP вызывающей процедуры;

q  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 операций, большинство из которых являются стековыми: операнды извлекаются из стека и результат помещается в стек. Команды имеют следующие форматы:

q  КОП P Q

q  КОП P

q  КОП Q

q  КОП

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

Команды без параметров обычно применяются к одному или двум элементам стека, и результат помещают снова в стек. В качестве примеров можно привести следующие операции:

q  ADR — сложение вещественных; извлекает два верхних элемента стека, которые являются вещественными числами, выполняет сложение и помещает результат в стек;

q  INN — проверка принадлежности множеству; извлекает из стека два верхних элемента, которые представляют собой множество и элемент; выполняется проверка принадлежности элемента множеству и на вершину стека помещается булевское значение;

q  FLT — преобразовать целое на вершине стека в вещественное;

Команды загрузки-сохранения и пересылки содержат, как правило, один или два аргумента, например:

q  LDCI 4 — загрузка целой константы 4 в стек;

q  LODI 0 5 — загружает в стек целое значение из слов с адресом (0,5);

q  LDA 0 6 — загрузка в стек адреса (0,6);

q  STRI 1 5 — сохранить значение с вершины стека в слове с адресом (1,5);

q  MOV 10 — извлечь из стека исходный и целевой адрес и переслать 10 слов, начиная с исходного адреса по целевому адресу;

В состав системы команд входит несколько команд перехода, например:

q  UJP L2 — безусловный переход по метке L2;

q  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 являются:

q  Загрузчик классов, который загружает, связывает и инициализирует классы;

q  Исполнитель (интерпретатор), который и выполняет программу в байт-кодах;

q  Интерфейс потоков, управляющий параллельной работой;

q  Модуль управления памятью, который управляет кучей, где хранятся объекты и массивы;

q  Модуль управления обработкой исключительных ситуаций, систематически отслеживающий возникающие в процессе выполнения исключительные ситуации и ошибки;

q  Модуль управления защитой, который препятствует запуску «враждебных» программ.

Каждый класс-файл содержит байт-код всех описанных методов, таблицу имен, таблица связей с суперклассами и т. д. Вся эта информация записана в единственной структуре[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-машины является верификация класс-файла перед выполнением. Верификатор контролирует корректность структуры класс-файла, осуществляет проверку байт-кода. В частности, проверяется:

q  наличие команд передачи управления, обращающихся к некорректным адресам;

q  наличие методов, вызываемых с неправильными аргументами;

q  некорректное управление стеком при опустошении и переполнении;

q  корректность типов аргументов в командах.

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

Типы данных 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 объединяются в следующие группы:

q  загрузка-сохранение;

q  арифметические и битовые команды;

q  преобразование типов;

q  оперирование объектами и массивами;

q  работа со стеком;

q  передача управления;

q  вызов методов;

q  обработка исключительных ситуаций и параллельная работа.

Архитектура 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

Разработка виртуальных машин

Разработка виртуальной машины состоит в разработке ее архитектуры. Для этого требуется определить несколько моделей: модель памяти, модель регистров, модели данных, модель процессора и алгоритм обработки команд, модели команд и алгоритм выполнения каждой команды. Если требуется, то определяется модель стека.

Абстрактная машина AMF

В качестве первого примера определим простую абстрактную машину AMF (Abstract Machine First).

Память AMF состоит из 512 ячеек-слов с адресами от 0 до 511. Одно слово памяти содержит 32 бита. В каждом слове, кроме нулевого, может быть записано целое число, вещественное число или команда. В нулевой ячейке всегда записан ноль: все биты равны 0.

Целые числа являются знаковыми, отрицательные числа представляются в дополнительном коде. Вещественные числа представляются как короткие вещественные числа по стандарту IEEE-754 [_]. В соответствии с этим стандартом вещественный ноль представляется точно так же, как целый: все биты слова равны 0.

Команды имеют следующий формат:

q  код операции (5 разрядов);

q  А1, А2, А3 – адреса операндов (каждый адрес по 9 разрядов).

Код операции представляет собой число от 0 до 31, а адресная часть составляет 27 бит. В соответствии с количеством адресов, машина AMF является 3-адресной[6]. Состав операций представлен в табл. 1.1. В таблице определены только 21 команда из 32. Это дает нам возможность добавить при необходимости несколько новых команд. Для удобства код каждой операции обозначается мнемоническим обозначением.

Регистры процессора:

q  RA – регистр адреса команды, в котором процессор хранит адрес выполняемой команды; содержит 9 разрядов;

q  RC – регистр команды, в котором процессор хранит выбранную из памяти и выполняемую в данный момент команду;

q  Flag – регистр признака результата, в который процессор записывает целое число как результат некоторых операций: -1, если результат отрицательный, 0 – если результат нулевой и +1, если результат положительный;

q  Error – регистр ошибки: 0 – ошибки при выполнении операции отсутствуют, 1 – при выполнении операции произошла ошибка;

Таблица 1.1

Код операции (КОП)

Комментарий

00

Stop – остановка работы процессора

01

Iadd – сложение целых чисел

02

Isub – вычитание целых чисел

03

Imul – умножение целых чисел

04

Idiv – деление целых чисел

05

Imod – остаток от деления целых чисел

08

Iin – ввод целых чисел

09

Iout – вывод целых чисел

10

ItoR – перевод целого в вещественное

11

Radd – сложение вещественных чисел

12

Rsub – вычитание вещественных чисел

13

Rmul – умножение вещественных чисел

14

Rdiv – деление вещественных чисел

16

Rcmp – сравнение вещественных чисел

18

Rin – ввод вещественных чисел

19

Rout – вывод вещественных чисел

20

RtoI – перевод вещественного в целое

25

Move – пересылка (копирование)

26

Xchg – обмен содержимым двух слов

30

Go – безусловный переход

31

Jmp – условный переход

Для описания алгоритма работы процессора и алгоритмов выполнения команд введем некоторые формальные обозначения. Память представляет собой массив mem[512]. Если х — это адрес, то mem[x] представляет собой содержимое ячейки памяти с адресом x.

Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4