Реклама:

Сейчас мы уже дошли до того момента, когда можно соединить все части вместе. В табл. 4.3 приводится микропрограмма, которая работает на микроархитектуре Mic-1 и интерпретирует IJVM. Программа очень короткая - всего 112 микрокоманд. Таблица состоит из трех столбцов. В первом столбце дано символическое обозначение микрокоманды, во втором - сама микрокоманда, в третьем - комментарий. Как мы уже отмечали, последовательность микрокоманд не обязательно соответствует последовательности адресов в управляющей памяти.

Таблица 4.3. Микропрограмма для микроархитектуры Mic-1

Микрокоманда

Операции

Комментарий

Maini

РС = РС+ 1; fetch; goto(MBR)

МВР содержит код операции; получение следующего байта; отсылка

порі

goto Maini

Ничего не происходит

iadcM

MAR = SP = SP - 1; rd

Чтение слова, идущего после верхнего слова стека

iadd2

H = TOS

Н = вершина стека

iadd3

MDR = TOS = MDR + H; wr; goto Maini

Суммирование двух верхних слов; запись суммы в верхнюю позицию стека

isubl

MAR = SP = SP - 1; rd

Чтение слова, идущего после верхнего слова стека

isub2

H = TOS

Н = вершина стека

isub3

MDR = TOS = MDR - H; wr; goto Maini

Вычитание; запись результата в вершину стека

Микрокоманда

Операции

Комментарий

iancM

MAR = SP = SP - 1; rd

Чтение слова, идущего после верхнего слова стека

iand2

H = TOS

Н = вершина стека

iand3

MDR = TOS = MDR И H; wr; goto Mainl

Операция И; запись результата в вершину стека

ior1

MAR = SP = SP - 1; rd

Чтение слова, идущего после верхнего слова стека

ior2

H = TOS

Н = вершина стека

іогЗ

MDR = TOS = MDR ИЛИ H; wr; goto Mainl

Операция ИЛИ; запись результата в вершину стека

dupl

MAR = SP = SP + 1

Увеличение SP на 1 и копирование результата в регистр MAR

dup2

MDR = TOS; wr; goto Mainl

Запись нового слова в стек

pop1

MAR = SP = SP - 1; rd

Чтение слова, идущего после верхнего слова стека

pop2

 

Программа ждет, пока считается из памяти новое значение регистра TOS

рорЗ

TOS = MDR; goto Mainl

Копирование нового слова в регистр TOS

swapl

MAR = SP = SP - 1; rd

Установка регистра MAR на значение SP -1; чтение второго слова из стека

swap2

MAR = SP

Установка регистра MAR на верхнее слово стека

swap3

H = MDR; wr

Сохранение значения TOS в регистре Н; запись второго слова в вершину стека

swap4

MDR = TOS

Копирование прежнего значения TOS в регистр MDR

swap5

MAR - SP - 1; wr

Установка регистра MAR на значение SP-1; запись второго слова в стек

swap6

TOS = H; goto Mainl

Обновление TOS

bipushl

SP = MAR = SP + 1

MBR = байт, который нужно поместить в стек

bipush2

PC = PC + 1; fetch

Увеличение PC на 1; вызов кода следующей операции

bipush3

MDR = TOS = MBR; wr; goto Mainl

Добавление к байту дополнительного знакового разряда и запись значения в стек

iloadl

H = LV

MBR содержит индекс; копирование значения LV в Н

iload2

MAR = MBRU + H; rd

MAR = адрес локальной переменной, которую нужно поместить в стек

iload3

MAR = SP = SP +1

Регистр SP указывает на новую вершину стека; подготовка к записи

iload4

PC = PC + 1; fetch; wr

Увеличение значения PC на 1; вызов кода следующей операции; запись вершины стека

Продолжение &

Таблица 4.3 (продолжение)

Микрокоманда

Операции

Комментарий

iload5

TOS = MDR; goto Mainl

Обновление TOS

istorel

H = LV

MBR содержит индекс; копирование значения LV в Н

istore2

MAR = MBRU + H

MAR = адрес локальной переменной, в которой нужно сохранить слово из стека

istore3

MDR = TOS; wr

Копирование значения TOS в регистр MDR; запись слова

istore4

SP= MAR = SP - 1; rd

Чтение из стека второго слова сверху

istore5

PC = PC + 1; fetch

Увеличение PC на 1; вызов следующего кода операции

istore6

TOS = MDR; goto Mainl

Обновление TOS

widel

PC = PC+ 1;fetch

Вызов байта операнда или кода следующей операции

wide2

goto(MBR ИЛИ 0x100)

Межуровневый переход к старшим адресам

widejloadl

PC = PC + 1 ;fetch

MBR содержит первый байт индекса; вызов второго байта

wide_iload2

H = MBRU " 8

Н = первый байт индекса, сдвинутый влево на 8 бит

wide_iload3

H=MBRU ИЛИ H

Н = 16-разрядный индекс локальной переменной

wide_iload4

MAR = LV + H; rd; goto iload3

MAR = адрес локальной переменной, которую нужно записать в стек

widejstorel

PC = PC + 1 ;fetch

MBR содержит первый байт индекса; вызов второго байта

wide_istore2

H = MBRU " 8

Н = первый байт индекса, сдвинутый влево на 8 бит

wide_istore3

H = MBRU ИЛИ H

Н = 16-разрядный индекс локальной переменной

wide_istore4

MAR = LV + H; rd; goto istore3

MAR = адрес локальной переменной, в которую нужно записать слово из стека

Idc_w1

PC = PC + 1;fetch

MBR содержит первый байт индекса; вызов второго байта

Idc_w2

H = MBRU " 8

Н = первый байт индекса, сдвинутый влево на 8 бит

Idc_w3

H = MBRU ИЛИ H

Н = 16-разрядный индекс константы в наборе констант

Idc_w4

MAR = H + CPP; rd; goto iload3

MAR = адрес константы в наборе констант

und

H = LV

MBR содержит индекс; копирование значения LV в Н

iinc2

MAR = MBRU+H; rd

Копирование суммы значения LV и индекса в регистр MAR; чтение переменной

ііпсЗ

PC = PC + 1; fetch

Вызов константы

Микрокоманда

Операции

Комментарий

iinc4

H = MDR

Копирование переменной в регистр Н

iinc5

PC = PC + 1; fetch

Вызов следующего кода операции

ііпсб

MDR = MBR + H; wr; goto Mainl

Запись суммы в регистр MDR; обновление переменной

gotol

OPC = PC - 1

Сохранение адреса кода операции

goto2

PC = PC + 1; fetch

MBR = первый байт смещения; вызов второго байта

goto3

H = MBR " 8

Сдвиг первого байта влево на 8 бит и сохранение его в регистре Н

goto4

H = MBRU ИЛИ H

Н =16-разрядное смещение перехода

goto5

PC = OPC + H; fetch

Суммирование смещения и ОРС

goto6

goto Mainl

Ожидание вызова следующего кода операции

ifltl

MAR = SP = SP - 1; rd

Чтение второго сверху слова в стеке

iflt2

OPC = TOS

Временное сохранение TOS в ОРС

iflt3

TOS = MDR

Запись новой вершины стека в TOS

iflt4

N = OPC; if(N) goto T; else goto F

Переход по биту N

ifeql

MAR = SP = SP - 1; rd

Чтение второго сверху слова в стеке

ifeq2

OPC = TOS

Временное сохранение TOS в ОРС

ifeq3

TOS = MDR

Запись новой вершины стека в TOS

ifeq4

Z = OPC; if(Z) goto T; else goto F

Переход по биту Z

if_icmpeq1

MAR = SP = SP - 1; rd

Чтение второго сверху слова в стеке

if_icmpeq2

MAR = SP = SP - 1

Установка регистра MAR на чтение новой вершины стека

if_icmpeq3

H = MDR; rd

Копирование второго слова из стека в регистр Н

if_icmpeq4

OPC = TOS

Временное сохранение TOS в ОРС

if_icmpeq5

TOS = MDR

Помещение новой вершины стека в TOS

if_icmpeq6

Z = OPC - H; if(Z) goto T; else goto F

Если два верхних слова равны, осуществляется переход к Т; если они не равны, осуществляется переход к F

T

OPC = PC - 1; fetch; goto goto2

То же, что gotol; нужно для адреса целевого объекта

F

PC = PC + 1

Пропуск первого байта смещения

F2

PC = PC + 1; fetch

PC указывает на следующий код операции

F3

goto Mainl

Ожидание вызова кода операции

invoke_virtual1

PC = PC + 1; fetch

MBR = первый байт индекса; увеличение PC на 1; вызов второго байта

invoke_virtua!2

H = MBRU " 8

Сдвиг первого байта на 8 бит и сохранение значения в регистре Н

Продолжение &

Таблица 4.3 {продолжение)

Микрокоманда

Операции

Комментарий

туоке^гшаЮ

Н = МВРШ или н

Н = смещение указателя процедуры от регистра срр

\п\/оке_\/\Пиа\4

МА13 = срр+Н; гс1

Вызов указателя процедуры из набора констант

1гмэке_у1гт.иа15

орс = рс + 1

Временное сохранение значения рс в регистре ОРС

туоке_>л11иа16

рс = мор; теЮп

Регистр рс указывает на новую процедуру; вызов числа параметров

т70ке_у1гт.иа17

рс = рс + 1; те1сп

Вызов второго байта числа параметров

туоке_>лПиа18

Н = МВРШ " 8

Сдвиг первого байта на 8 бит и сохранение значения в регистре н

т70ке_мгт.иа19

н = мвпи ИЛИ Н

н = число параметров

туоке^НиаИО

рс = рс+ 1;тет.сп

Вызов первого байта размера области локальных переменных

1пуоке_у||1иа111

ТОЭ = эр - Н

ТОЭ = адрес ОВ0РЕР-1

туоке_уНиа112

ТОЭ = МА13 = ТОЭ + 1

ТОЭ = адрес ОБ^ЕР (новое значение

тгоке.мгШаИЗ

рс = рс + ^',1e\ch

Вызов второго байта размера области локальных переменных

1т/оке_мгша114

Н = МВРШ " 8

Сдвиг первого байта на 8 бит и сохранение значения в регистре н

1т/оке_у1Пиа115

Н = МВРШ или н

н = размер области локальных переменных

туоке_ун1иа116

мов = 8Р + Н + 1\ш

Перезапись ОВЛЧЕР со связующим указателем

1пуоке_>лгШа117

МАЯ = эр = МОЯ

Установка регистров эр и МАЯ на адрес ячейки, в которой содержится старое значение рс

1т/оке_у1гша118

М013 = ОРС; ш

Сохранение старого значения рс над локальными переменными

1П70ке_у'1Гт.иа119

МАЯ = бр = эр + 1

эр указывает на ячейку, в которой хранится старое значение

1т/оке_мгт.иа120

М013 = 1Л/; ш

Сохранение старого значения над сохраненным значением рс

т7оке_у1гг.иа121

рс = рс + ^^,tetcb

Вызов первого кода операции новой процедуры

1тгоке_у1гг.иа122

О/ = ТОЭ; до1о Мат1

Установка значения на первый адрес фрейма локальных переменных

1геШт1

МА13 = эр = 1Л/; гб

Переустановка регистров эр и МАЯ для вызова связующего указателя

\retum2

 

Процесс считывания

^еШгпЗ

14 = МАЯ = мр^ гс1

Установка регистра на связующий указатель; вызов старого значения рс

1геШгп4

МАЯ = 1Л/ + 1

Установка регистра МАР на чтение старого значения

Микрокоманда

Операции

Комментарий

ireturn5

PC = MDR; rd; fetch

Восстановление PC; вызов следующего кода операции

ireturn6

MAR = SP

Установка MAR на запись TOS

ireturn7

LV = MDR

Восстановление LV

ireturn8

MDR = TOS; wr; goto Mainl

Сохранение результата в исходной вершине стека

Выбор названий для большинства регистров, изображенных на рис. 4.1, должен быть очевидным. Регистры СРР (Constant Pool Pointer - указатель набора констант), LV (Local Variable - локальная переменная) и SP (Stack Pointer - указатель стека) содержат указатели на адреса набора констант, фрейма локальных переменных и верхнего элемента в стеке соответственно, а регистр РС (Program Counter - счетчик команд) содержит адрес байта, который нужно вызвать из потока команд следующим. Регистр MBR (Memory Buffer Register - буферный регистр памяти) - это однобайтный регистр, который содержит байты потока команд, поступающих из памяти для интерпретации. TOS и ОРС - дополнительные регистры. Они описываются далее.

В определенные моменты в каждом из этих регистров обязательно находится определенное значение. Однако каждый из них в случае необходимости может также использоваться в качестве временного. В начале и конце каждой команды регистр TOS (Top Of Stack - вершина стека) содержит значение адреса памяти, на который указывает SP. Это значение избыточно, поскольку его всегда можно считать из памяти, но если хранить это значение в регистре, то обращение к памяти не потребуется. Для некоторых команд использование регистра TOS, напротив, влечет за собой большее количество обращений к памяти. Например, команда POP отбрасывает верхнее слово стека и, следовательно, должна вызвать новое значение вершины стека из памяти и записать его в регистр TOS.

ОРС - временный регистр. У него нет определенного назначения. В нем, например, может храниться адрес кода операции для команды перехода, пока значение РС увеличивается, чтобы получить доступ к параметрам. Он также используется в качестве временного регистра в командах условного перехода.

Как и все интерпретаторы, микропрограмма, приведенная в табл. 4.3, включает в себя основной цикл, который вызывает, декодирует и выполняет команды интерпретируемой программы (в данном случае IJVM-команды). Основной цикл начинается со строки Mainl, а именно - с инварианта (утверждения), что в регистр РС уже загружен адрес ячейки памяти, в которой содержится код операции. Более того, этот код операции уже вызван из памяти в регистр MBR. Когда мы вернемся к этой ячейке, мы должны быть уверены, что значение РС уже обновлено и указывает на код следующей операции, а сам код операции уже вызван из памяти в MBR.

Такая последовательность действий имеет место в начале каждой команды, поэтому важно сделать ее как можно более короткой. Разрабатывая аппаратное и программное обеспечение микроархитектуры Mic-1, мы смогли сократить основной цикл до одной микрокоманды. Каждый раз, когда выполняется эта микрокоманда, код выполняемой операции уже находится в регистре MBR. Эта микрокоманда, во-первых, осуществляет переход к микрокоду, выполняющему данную операцию, во-вторых, вызывает следующий после кода операции байт, который может быть либо операндом, либо кодом операции.

Теперь мы можем объяснить главную причину того, почему в каждой микрокоманде явным образом указывается следующая микрокоманда, а последовательность команд может не соответствовать порядку их расположения в памяти. Все адреса управляющей памяти, соответствующие кодам операций, должны быть зарезервированы для первого слова интерпретатора соответствующей команды. Так, из табл. 4.2 мы видим, что программа, которая интерпретирует команду POP, начинается в ячейке 0x57, а программа, которая интерпретирует команду DUP, начинается в ячейке 0x59. (Как язык MAL узнает, что команду POP нужно поместить в ячейку 0x57 - одна из загадок Вселенной. Предположительно, где-то существует файл, который сообщает ему об этом.)

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

Чтобы понять, как работает интерпретатор, предположим, что регистр MBR содержит значение 0x60, то есть код операции IADD (см. табл. 4.2). В основном цикле, который состоит из одной микрокоманды, выполняется следующее:

1. Значение регистра PC увеличивается, после чего он содержит адрес первого байта после кода операции.

2. Начинается передача следующего байта в регистр MBR. Этот байт понадобится рано или поздно либо в качестве операнда текущей IJVM-команды, либо в качестве кода следующей операции (как в случае с командой IADD, у которой нет операндов).

3. Совершается переход к адресу, который содержался в регистре MBR в начале цикла Mai ni. Номер адреса равен значению кода операции, которая выполняется в данный момент. Этот адрес помещается туда предыдущей микрокомандой. Отметим, что значение, которое вызывается из памяти во время этой микрокоманды, не играет никакой роли в межуровневом переходе.

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

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

Еще раз подчеркнем, что микрокоманды, приведенные в табл. 4.3, расположены в памяти не последовательно, а микрокоманда Mainl вовсе не находится в ячейке с адресом 0 (поскольку в этой ячейке должна находиться микрокоманда nopl). Задача микроассемблера - поместить каждую команду в подходящую ячейку и связать их в короткие последовательности, использовав поле NEXT_ADDRESS. Каждая последовательность начинается с адреса, который соответствует численному значению кода операции (например, команда POP начинается с адреса 0x57), но остальные части последовательности могут находиться в любых ячейках управляющей памяти, и эти ячейки не обязательно смежные.

А теперь рассмотрим команду IADD. Она начинается с микрокоманды iaddl. Требуется выполнить следующие действия:

1. Значение регистра TOS уже есть, но из памяти нужно вызвать второе слово стека.

2. Значение регистра TOS нужно прибавить ко второму слову стека, вызванному из памяти.

3. Результат, который помещается в стек, должен быть сохранен в памяти и в регистре TOS.

Для того чтобы вызвать операнд из памяти, необходимо уменьшить значение указателя стека и записать его в регистр MAR. Отметим, что этот адрес будет использоваться для последующей записи. Более того, поскольку эта ячейка памяти станет новой вершиной стека, данное значение должно быть присвоено регистру SP. Следовательно, определить новое значение SP и MAR, уменьшить значение SP на 1 и записать его в оба регистра можно за одну операцию.

Все эти действия выполняются в первом цикле (iaddl). Здесь же инициируется операция чтения. Кроме того, регистр MPC получает значение из поля NEXT_ ADDRESS микрокоманды iaddl. Это адрес микрокоманды iadd2. Затем iadd2 считывается из управляющей памяти. Во втором цикле, пока происходит считывание операнда из памяти, мы копируем верхнее слово стека из TOS в Н, где оно будет доступно для сложения, когда процесс считывания завершится.

В начале третьего цикла (iadd3) MDR содержит второе слагаемое, вызванное из памяти. В этом цикле оно прибавляется к значению регистра H, а результат сохраняется обратно в регистрах MDR и TOS. Кроме того, начинается операция записи, в процессе которой новое верхнее слово стека сохраняется в памяти. В этом цикле команда goto приписывает адрес Mainl регистру MPC, и таким образом мы возвращаемся к исходному пункту, и можем начать выполнение следующей операции.

Если следующий код IJVM-операции, который содержится в данный момент в регистре MBR, равен 0x64 (ISUB), то повторяется практически та же последовательность действий. После выполнения Mainl управление передается микрокоманде с адресом 0x64 (isubl). За этой микрокомандой следуют isub2, isub3, а затем снова Mainl. Единственное различие между этой и предыдущей последовательностями состоит в том, что в цикле isub3 содержимое регистра H не прибавляется к значению MDR, а вычитается из него.

Команда IAND идентична командам IADD и ISUB, только в данном случае два верхних слова стека подвергаются логическому умножению (операция И), а не складываются и не вычитаются. Нечто подобное происходит и во время выполнения команды I0R.

Если код операции соответствует команде DUP, POP или SWAP, то нужно использовать стек. Команда DUP дублирует верхнее слово стека. Поскольку значение этого слова уже находится в регистре TOS, нужно просто увеличить SP на 1. После этого регистр SP будет указывать на новый адрес. В эту новую ячейку и записывается значение регистра TOS. Команда POP тоже достаточно проста: нужно только уменьшить значение SP на 1, чтобы отбросить верхнее слово стека. Однако после этого необходимо считать новое верхнее слово стека из памяти и записать его в регистр TOS. Наконец, команда SWAP меняет местами значения двух ячеек памяти, а именно - два верхних слова стека. Регистр TOS уже содержит одно из этих значений, поэтому считывать его (значение) из памяти не нужно. Подробнее мы обсудим эту команду немного позже.

Команда BIPUSH сложнее предыдущих, поскольку за кодом операции следует байт, как показано на рис. 4.13. Этот байт представляет собой целое число со знаком. Указанный байт, который был передан в регистр MBR во время выполнения микрокоманды Mai ni, нужно расширить до 32 бит (знаковое расширение) и скопировать его в регистр MDR. Затем значение SP увеличивается на 1 и копируется в MAR, что позволяет записать операнд на вершину стека. Этот операнд также должен копироваться в регистр TOS. Отметим, что значение регистра PC требуется увеличить на 1, чтобы в микрокоманде Mai ni имелся код следующей операции.

Реализация IJVM с использованием микроархитектуры Mic-1

Рис. 4.13. Формат команды BIPUSH

Теперь рассмотрим команду IL0AD. В этой команде за кодом операции также следует байт (рис. 4.14, а), но этот байт представляет собой индекс (без знака), используемый для того, чтобы найти в пространстве локальных переменных слово, которое нужно поместить в стек. Поскольку здесь имеется всего 1 байт, доступно только 28 = 256 слов, а именно первые 256 слов пространства локальных переменных. Для выполнения команды IL0AD требуются и чтение (чтобы вызвать слово), и запись (чтобы поместить его в стек). Чтобы определить адрес для считывания, нужно прибавить смещение, которое хранится в регистре MBR (это буферный регистр памяти), к содержимому регистра LV. Доступ к регистрам MBR и LV можно получить только через шину В, поэтому сначала значение LV копируется в регистр Н (в цикле iloadl), а затем прибавляется значение MBR. Результат суммирования копируется в регистр MAR, и начинается процесс чтения (в цикле iload2).

Реализация IJVM с использованием микроархитектуры Mic-1

Рис. 4.14. Команда ILOAD с однобайтным индексом (а); команда WIDE ILOAD с двухбайтным индексом (б)

Однако здесь регистр MBR используется не совсем так, как в команде BIPUSH, где байт расширен по знаку. В случае с индексом смещение всегда положительно, поэтому байт смещения должен быть целым числом без знака (в отличие от BIPUSH, где байт представляет собой 8-разрядное целое число со знаком). Интерфейс между регистром MBR и шиной В разработан таким образом, чтобы были возможны обе операции. В случае с командой BIPUSH (где байт - 8-разрядное целое число со знаком) самый левый бит значения MBR копируется в 24 старших бита шины В. В случае с командой IL0AD (где байт - 8-разрядное целое число без знака) 24 старших бита шины В заполняются нулями. Два специальных сигнала помогают определить, какую из этих двух операций нужно выполнить (см. рис. 4.5). В микропрограмме слово MBR указывает на байт со знаком (как в команде bipush3), a MBRU - на байт без знака (как в команде iload2).

Пока ожидается поступление операнда из памяти (во время iload3), значение регистра SP увеличивается на 1 для записи новой вершины стека. Это значение также копируется в регистр MAR (это требуется для записи операнда в стек). Затем значение РС снова увеличивается на 1, чтобы вызвать следующий код операции (микрокоманда iload4). Наконец, значение MDR копируется в регистр TOS, чтобы показать новое верхнее слово стека (микрокоманда iload5).

Команда ISTORE противоположна команде IL0AD (из стека выталкивается верхнее слово и сохраняется в ячейке памяти, адрес которой равен сумме значения регистра LV и индекса данной команды). В данном случае используется такой же формат, как и в команде IL0AD (см. рис. 4.14, а), только здесь код операции равен не 0x15, а 0x36. Поскольку верхнее слово стека уже известно (оно находится в регистре TOS), его можно сразу сохранить в памяти. Однако новое верхнее слово стека все же необходимо вызвать из памяти, поэтому требуются и чтение, и запись, хотя эти операции можно выполнять в любом порядке (или даже одновременно, если бы это было возможно).

Команды IL0AD и ISTORE имеют доступ только к первым 256 локальным переменным. Хотя для большинства программ этого пространства достаточно, все же нужно иметь возможность обращаться к любой локальной переменной, в какой бы части фрейма она ни находилась. Чтобы обеспечить такую возможность, IJVM использует то же средство, что и JVM, - специальный код операции WIDE (так называемый префиксный байт), за которым следует код операции IL0AD или ISTORE. Когда встречается такая последовательность, формат команды IL0AD или ISTORE меняется; в соответствии с новым форматом за кодом операции следует не 8-разрядный, а 16-разрядный индекс, как показано на рис. 4.14, б.

Команда WIDE декодируется обычным способом. Сначала происходит переход к микрокоманде widel, которая обрабатывает код операции команды WIDE. Хотя код операции, который нужно расширить, уже присутствует в регистре MBR, микрокоманда widel вызывает первый байт после кода операции, поскольку этого требует логика микропрограммы. Затем совершается еще один межуровневый переход, но на этот раз для перехода используется байт, следующий за WIDE. Однако, поскольку команда WIDE IL0AD требует иного, чем IL0AD, набора микрокоманд, команда WIDE ISTORE - иного, чем ISTORE, и т. д., при межуровневом переходе код операции нельзя использовать в качестве целевого адреса.

Вместо этого микрокоманда widel подвергает логическому сложению адрес 0x100 и код операции, поместив его в регистр MPC. В результате интерпретация WIDE LOAD начинается с адреса 0x115 (а не 0x15), интерпретация WIDE IST0RE - с адреса 0x136 (а не 0x36) и т. д. Таким образом, каждый код операции WIDE начинается с адреса, который в управляющей памяти на 256 (то есть 0x100) слов выше, чем соответствующий код обычной операции. Начальная последовательность микрокоманд для IL0AD и WIDE IL0AD показана на рис. 4.15.

Реализация IJVM с использованием микроархитектуры Mic-1

Рис. 4.15. Начало последовательности микрокоманд для команд ILOAD и WIDE ILOAD. Адреса приводятся в качестве примера

Команда WIDE ILOAD отличается от обычной команды IL0AD только тем, что индекс в ней состоит из двух индексных байтов. Слияние и последующее суммирование этих байтов должно происходить поэтапно, при этом сначала первый индексный байт сдвигается влево на 8 бит и копируется в Н. Поскольку индекс - целое число без знака, здесь используется регистр MBRU (24 старших бита заполняются нулями). Затем прибавляется второй байт индекса (операция сложения идентична слиянию, поскольку младший байт регистра Н в данный момент равен 0), при этом гарантируется, что между байтами не будет переноса. Результат снова сохраняется в регистре Н. С этого момента происходят те же действия, что и в стандартной команде IL0AD. Вместо того чтобы дублировать последние микрокоманды (от iload3 до iload5) команды IL0AD, мы просто совершили переход от wide_iload4 к iload3. Отметим, что во время выполнения этой команды значение PC должно увеличиваться на 1 дважды, чтобы в конце этот регистр указывал на следующий код операции. Команда IL0AD увеличивает значение один раз; последовательность команд WIDEIL0AD также увеличивает это значение один раз.

Такая же ситуация имеет место при выполнении WIDEISTORE. После первых четырех микрокоманд (от wideistorel до wide_istore4) последовательность действий та же, что и в команде ISTORE после первых двух микрокоманд, поэтому мы совершаем переход от wide_istore4 к istore3.

Далее мы рассмотрим команду LDCW. Есть два отличия этой команды от IL0AD. Во-первых, она содержит 16-разрядное смещение без знака (как и расширенная версия IL0AD), во-вторых, эта команда индексируется из регистра СРР, а не из LV, поскольку она считывает значение из набора констант, а не из фрейма локальных переменных. (Существует еще и краткая форма этой команды - LDC, но мы не стали включать ее в машину IJVM, поскольку полная форма содержит в себе все варианты краткой формы, хотя при этом занимает 3 байта вместо 2.)

Команда IINC - единственная команда, помимо ISTORE, которая может изменять локальную переменную. Она включает в себя два операнда по одному байту, как показано на рис. 4.16.

Реализация IJVM с использованием микроархитектуры Mic-1

Рис. 4.16. Команда IINC содержит два поля операндов

Поле индекса нужно для того, чтобы определить смещение от начала фрейма локальных переменных. Команда считывает эту переменную, увеличивает ее на константу (константа содержится во втором поле) и помещает результат обратно в ту же ячейку памяти. Отметим, что константа является 8-разрядным числом со знаком в промежутке от -128 до +127. Машина JVM поддерживает расширенную версию этой команды, в которой длина каждого операнда составляет 2 байта.

Рассмотрим первую команду перехода - GOTO. Эта команда изменяет значение регистра РС таким образом, чтобы следующая IJVM-команда находилась в ячейке памяти с адресом, который вычисляется путем прибавления 16-разрядного смещения (со знаком) к адресу кода операции GOTO. Сложность здесь в том, что смещение связано со значением, находящимся в регистре РС в начале декодирования команды, а не тем, которое содержится в том же регистре после вызова 2 байт смещения.

Чтобы лучше это понять, посмотрите на рис. 4.17, а. Здесь показана ситуация, которая имеет место в начале цикла Mainl. Код операции уже находится в регистре MBR, но значение РС еще не увеличилось. На рис. 4.17, б мы видим ситуацию в начале цикла gotol. В данном случае значение РС уже увеличено на 1, а первый байт смещения уже передан в MBR. В следующей микрокоманде (рис. 4.17, в) прежнее значение РС, которое указывает на код операции, сохраняется в регистре ОРС. Это значение требуется сохранять, поскольку именно от него, а не от текущего значения РС зависит смещение команды GOTO. И именно для этого предназначен регистр ОРС.

Микрокоманда goto2 начинает вызов второго байта смещения, что приводит к ситуации, показанной на рис. 4.17, г (микрокоманда goto3). После того как первый байт смещения сдвигается влево на 8 бит и копируется в регистр Н, мы переходим к микрокоманде goto4 (рис. 4.17, Э). Теперь у нас первый байт смещения, сдвинутый влево, находится в регистре Н, второй байт смещения - в регистре MBR, а основание смещения - в регистре ОРС. В микрокоманде goto5 путем прибавления полного 16-разрядного смещения к основанию смещения мы получаем новый адрес, который помещается в регистр PC. Отметим, что в goto4 вместо MBR мы используем регистр MBRU, поскольку нам не требуется знаковое расширение второго байта. 16-разрядное смещение строится путем логического сложения (операция ИЛИ) двух половинок. Наконец, поскольку программа перед переходом к Mai ni требует, чтобы в MBR был помещен код следующей операции, мы должны вызвать этот код. Последний цикл, goto6, нужен для того, чтобы вовремя поместить данные из памяти в регистр MBR.

Реализация IJVM с использованием микроархитектуры Mic-1

Рис. 4.17. Ситуация в начале выполнения различных микрокоманд: Mainl (a); gotol (б);

goto2 (е); goto3 (г); goto4 (д)

Смещения, которые используются в команде goto, представляют собой 16-разрядные значения со знаком в промежутке от -32 768 до +32 767. Это значит, что переходы на более дальние расстояния невозможны. Это свойство можно рассматривать либо как дефект, либо как особенность машины IJVM (а также JVM). Те, кто считает это дефектом, скажут, что машина JVM не должна ограничивать программиста. Те, кто считает это особенностью, скажут, что работа многих программистов продвинулась бы кардинальным образом, если бы им в ночных кошмарах приснилось следующее сообщение компилятора:

Программа слишком длинная и сложная. Вы должны переписать ее. Компиляция прекращена.

К сожалению (это наша точка зрения), это сообщение появится только в том случае, если объем предложения el se или then превысит 32 Кбайт, что составляет по крайней мере 50 страниц текста на языке Java.

А теперь рассмотрим три команды условного перехода: IFLT, IFEQ и IFICMPEQ. Первые две выталкивают верхнее слово из стека и совершают переход в том случае, если это слово меньше 0 или равно 0 соответственно. Команда IFICMPEQ берет два верхних слова из стека и совершает переход, если они равны. Во всех трех случаях необходимо считывать новое верхнее слово стека и помещать его в регистр TOS.

Эти три команды сходны. Сначала операнд или операнды помещаются в регистры, затем в TOS записывается новое верхнее слово стека, наконец, происходит сравнение и осуществляется переход. Сначала рассмотрим IFLT. Слово, которое нужно проверить, уже находится в регистре TOS, но поскольку команда IFLT выталкивает слово из стека, нужно считать из памяти новую вершину стека и сохранить ее в регистре TOS. Процесс считывания начинается в микрокоманде ifltl. Во время iflt2 проверяемое слово сохраняется в регистре ОРС, поэтому новое значение можно сразу поместить в регистр TOS, и при этом предыдущее значение не пропадет. В цикле i f 113 новое верхнее слово стека, которое уже находится в MDR, копируется в регистр TOS. Наконец, в цикле iflt4 проверяемое слово (оно находится в регистре ОРС) пропускается через АЛУ без сохранения результата, после чего проверяется бит N. Если после проверки условие подтверждается, микрокоманда осуществляет переход к Т, а если не подтверждается - к F.

Если условие подтверждается, то происходят те же действия, что и в начале команды GOTO, и далее осуществляется переход к goto2. Если условие не подтверждается, необходима короткая последовательность микрокоманд (F, F2 и F3), чтобы пропустить оставшуюся часть команды (смещение), возвратиться к Mai ni и перейти к следующей команде.

Команда IFEQ аналогична команде IFLT, только вместо бита N используется бит Z. В обоих случаях ассемблер должен убедиться, что адреса микрокоманд F и Т различаются только крайним левым битом.

Команда IF_ICMPEQ в целом похожа на команду IFLT, только здесь нужно считывать еще и второй операнд. Второй операнд сохраняется в регистре H во время цикла if_icmpeq3, где начинается чтение нового верхнего слова стека. Текущее верхнее слово стека сохраняется в ОРС, а новое загружается в регистр TOS. Наконец, микрокоманда if_icmpeq6 аналогична ifeq4.

Теперь рассмотрим команды INVOKEVIRTUAL и I RETURN. Как отмечалось в подразделе "Набор IJVM-команд" раздела "Пример архитектуры набора команд - IJVM", они служат для вызова процедуры и выхода из нее. Команда INVOKEVIRTUAL представляет собой последовательность из 22 микрокоманд. Это самая сложная команда машины IJVM. Последовательность действий при выполнении этой команды иллюстрирует рис. 4.10. 16-разрядное смещение используется для того, чтобы определить адрес вызываемой процедуры. Номер адреса процедуры находится в наборе констант. Следует помнить, что первые 4 байта каждой процедуры - не команды, а два 16-разрядных указателя. Первый из них позволяет узнать число параметров (включая OBJREF - см. рис. 4.10), второй - размер области локальных переменных (в словах). Эти поля вызываются через 8-разрядный порт и объединяются таким же образом, как 16-разрядное смещение в одной команде.

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

IRETURN - простая команда без операндов. Эта команда просто обращается к первому слову области локальных переменных, чтобы извлечь информацию для возвращения к прежнему состоянию. Затем она восстанавливает предыдущие значения регистров SP, LV и PC и копирует результат выполнения процедуры из нового стека в предыдущий стек, как показано на рис. 4.11.

Микрокоманды и их запись || Оглавление || Разработка уровня микроархитектуры