Подсистема JIT Паррота
Maintainer: Daniel Grunblatt
Class: Internals
PDD Number: 8
Version: 1.3
Status: Developing
Last Modified: 26 Nov 2002
PDD Format: 1
Language:English
Этот PDD описывает подсистему компиляции Паррота Just in Time(Как Раз Вовремя).
Подсистема Just In Time(или JIT) переводит файл с байт-кодом в инструкции родного машинного кода и выполняет сгенерированную последовательность.
В настоящий момент подсистема работает на процессорных системах ALPHA, Arm, Intel x86, PPC, and SPARC version 8 и на большинстве операционных системах. Пока поддерживаются только 32-разрядные значения INTVAL.
На начальном этапе генерации родного кода вызывается функция Parrot_jit_begin, которая обеспечивает архитектурно-специфический вводный код. Для каждого кода операции Паррота генерируется либо общая последовательность, либо специальная последовательность родного кода. Файлы с расширением .jit предоставляют функции, генерирующие родной код для специальных кодов операций с учетом имеющегося у архитектуры набора инструкций. Если для специального кода операции функция не предоставляется, то выводится общая последовательность родного кода, которая вызывает интерпретатор функций Си, который и реализует код операции. Такой код операции обрабатывается Parrot_jit_normal_op.
Если код операции может вызвать изменения в управляющем потоке, как в случае кодов операций branch и call, то используется расширенная или модифицированная версия общего кода, которая следит за изменениями программного и аппаратного счетчика. Данный тип кода операции обрабатывается Parrot_jit_cpcf_op.
Во время генерации родного кода могут быть не доступны точные смещения и абсолютные адреса. Это случается для кодов операций ветвления с переходом вперед, когда родный код соответствующий метке перехода еще не сгенерирован. На некоторых платформах вызовы функций выполняются с помощью программных счетчиков относительных адресов. Так как местоположение буфера, хранящего родной код может перемещаться по мере генерации кода(из-за увеличения буфера); относительные адреса могут быть вычислены после того как, гарантируется что буфер больше не будет перемещаться. Для обработки таких ситуаций подсистема JIT использует адресные записи, которые хранят местоположения в родном коде, требующие корректировки.
Для перемещения регистров от процессора к Парроту и обратно должны быть реализованы функции Parrot_jit_emit_mov*.
Массив op_jit структур jit_fn_info_t обеспечивает для каждого кода операции указатель на функцию, которая генерирует родной код. Функция является либо общей функцией Parrot_jit_normal_op, либо Parrot_jit_cpcf_op, либо специальной функцией для кода операции. Функция Parrot_jit_restart_op похожа на функцию Parrot_jit_cpcf_op с добавлением проверки нуля программного счетчика. Функции Parrot_jit_vtable*_op как Parrot_jit_normal_op или Parrot_jit_cpcf_op и могут быть реализованы для выполнения родных вызовов vtable (смотрите jit/i386/jit_emit.h для примера).
Структура Parrot_jit_fixup хранит смещение в родном коде, где должна быть произведена адресная запись, тип требуемой адресной записи и специальная информация необходимая для выполнения параметров записи. Сейчас параметр адресной записи является либо значением типа opcode_t, либо указателем на функцию.
Структура Parrot_jit_info хранит данные, используемые вовремя получения и исполнения родного кода. Важной частью данных в этой структуре является массив op_map, который отображает адреса кодов операций на адреса родного кода.
Байт-код по сути является массивом элементов размера opcode_t с параллельными записями op_map. На начальном этапе op_map заполнен смещениями в родном коде, соответствующими кодам операций в байт-коде. Как только генерация кода завершена и применины адресные записи, смещения в родном коде переводятся в абсолютные адреса. Таким образом удается обменять неопределенность затрат неоднократных преобразований смещений во время выполнения на небольшие затраты на начальном этапе.
Если архитектура определяет INT_REGISTERS_TO_MAP и FLOAT_REGISTERS_TO_MAP как не нулевые, то эти значения предельного числа используемых регистров отображается на родные регистры процессора.
Архитектурно-специфический файл jit_emit.h сообщает некоторые определения и таблицы с помощью jit.c и languages/imcc/imc.c. Поэтому структура файла и определения должны следовать специальному синтаксису.
#if JIT_EMIT
... emit код
#else
... определения
#ifndef JIT_IMCC
... инициализация отображений
... и возможно частные статические функции
#endif
#endif
jit_emit_noop(pc)
#define MAP(i) OMAP(i)
#undef MAP
#define MAP(i) (i) >= 0 : 0 ? OMAP(i)
# define EXTCALL(op) (op_jit[*(op)].extcall)
JIT/i386 имеет jit'ированные функции vtable, где extcall является номером записи vtable и (JIT/i386) переопределяет EXTCALL на:
# define EXTCALL(op) (op_jit[*(op)].extcall == 1)
Смотрите действительное применение этих определений в jit/i386/jit_emit.h
Jit-файлы интерпретируются следующим образом:
Закрывающая фигурная скоька должна находится в первой колонке.
INT_REG[n]
Замещается регистром INTVAL, указанным в аргументе n.
NUM_REG[n]
Замещается регистром FLOATVAL, указанным в аргументе n.
STRING_REG[n]
Замещается регистром STRING, указанным в аргументе n.
INT_CONST[n]
Замещается константой INTVAL, указанной в аргументе n.
NUM_CONST[n]
Замещается константой FLOATVAL, указанной в аргументе n.
MAP[n]
n-й целый или регистр с плавайщей точкой отображается в данной секции.
ЗАМЕЧАНИЕ: Регистр с физическим номером ноль не может быть отображен.
NATIVECODE
Замещается на значение текущего программного счетчика.
*CUR_OPCODE[n]
Замещается на адрес текущего кода операции байт-кода Паррота.
ISRn FSRn
Целый или с плавающей точкой регистр временной памяти.
Пример:
TEMPLATE Parrot_set_x_ic {
if (MAP[1]) {
jit_emit_mov_ri<_N>(NATIVECODE, MAP[1], <typ>_CONST[2]);
}
else {
jit_emit_mov_mi<_N>(NATIVECODE, &INT_REG[1], <typ>_CONST[2]);
}
}
Parrot_set_i_ic {
Parrot_set_x_ic s/<_N>/_i/ s/<typ>/*INT/
}
Parrot_set_n_ic {
Parrot_set_x_ic s/<_N>/_ni/ s/<typ>/&INT/ s/INT_R/NUM_R/
}
Jit-функция Parrot_set_i_ic основана на шаблоне Parrot_set_x_ic, s/x/y/ - замены, производимые в шаблоне для получения настоящего тела функции. Эти замены перед другими заменами.
Смотрите jit/i386/core.jit для получения дополнительной информации.
С целью упрощения распределения файлов core.jit между машинами со сходной архитектурой функции jit_emit должны следовать следующему синтаксису:
jit_emit_<op>_<args>_<type>
Примеры:
Доступ к регистрам Парота выполняется относительно $6, к памяти -
относительно $27, к константам с плавающей точкой - относительно to access float constants
relative to $7. То есть вы можете осуществлять контроль над инструкцией с
помощью ldah $7,0($27).
Поддерживаются только 32-разрядные значения INTVALs. Поддерживаются значения типа long double FLOATVAL.
Есть четыре отображаеммых регистра - %edi, %ebx, %esi и %edx. Первые три из них сохраняются для вызываемой стороны. Они предохраняют свои значения во время вызовов внешних функций.
Регистры ST1 ... ST4 четырех операций с плавающей точкой отображаются и рассматриваются как предохраняемые во время вызовов функций.
Давайте посмотрим как это работает:
Ассемблерный код Парота:
set I0,8 set I2,I0 print I2 end
Байт-код Парота: (показан только сегмент байт-кода)
+--------------------------------------+ | 73 | 0 | 8 | 72 | 2 | 0 | 21 | 2 | 0 | +-|------------|------------|--------|-+ | | | | | | | +----------- end (нет аргументов) | | +-------------------- print_i (1 аргумент) | +--------------------------------- set_i_i (2 аргумента) +---------------------------------------------- set_i_ic (2 аргумента)
Пожалуйста, обратите внимание: номера кодов операций уже могли поменяться, равно как и сгенерированный ассемблерный код может отличаться.
Версия ассемблера кодов операций для Intel x86:
Parrot_jit_begin
0x817ddd0 <jit_func>: push %ebp
0x817ddd1 <jit_func+1>: mov %esp,%ebp
0x817ddd3 <jit_func+3>: push %ebx
0x817ddd4 <jit_func+4>: push %esi
0x817ddd5 <jit_func+5>: push %edi
до сюда идет обычный заголовок функции,
теперь "заталкиваем" интерпретатор
0x817ddd6 <jit_func+6>: push $0x8164420
помещаем таблицу jit-функций в %ebp и
переходим к первой инструкции
0x817dddb <jit_func+11>: mov 0xc(%ebp),%eax
0x817ddde <jit_func+14>: mov $0x81773f0,%ebp
0x817dde3 <jit_func+19>: sub $0x81774a8,%eax
0x817dde9 <jit_func+25>: jmp *%ds:0x0(%ebp,%eax,1)
set_i_ic
0x817ddee <jit_func+30>: mov $0x8,%edi
set_i_i
0x817ddf3 <jit_func+35>: mov %edi,%ebx
Parrot_jit_save_registers
0x817ddf5 <jit_func+37>: mov %edi,0x8164420
0x817ddfb <jit_func+43>: mov %ebx,0x8164428
Parrot_jit_normal_op
0x817de01 <jit_func+49>: push $0x81774c0
0x817de06 <jit_func+54>: call 0x804be00 <Parrot_print_i>
0x817de0b <jit_func+59>: add $0x4,%esp
Parrot_jit_end
0x817de0e <jit_func+62>: add $0x4,%esp
0x817de14 <jit_func+68>: pop %edi
0x817de16 <jit_func+70>: pop %ebx
0x817de18 <jit_func+72>: pop %esi
0x817de1a <jit_func+74>: pop %ebp
0x817de1c <jit_func+76>: ret
Пожалуйста, обратите внимание на противоположную последовательность аргументов. Нотации PASM and JIT используют dest,src,src, в то время как gdb и внутренние макросы в jit_emit.h - src,dest.
Листинг вверху был сгенерирован gdb(отдладчик GNU) с небольшой помощью Parrot_jit_debug, который генерирует символьный файл в формате stabs. Смотрите info stabs за дополнительной информацией.
Следующий скрипт вызывает ddd (внешний графический отладчик) и прикрепляет символьной файл, после чего этот файл создается в build_asm.
# dddp
# запускаем ddd Парота с имеющимся файлом
# должны начинаться выводится подтверждения gdb
# $ ln -s languages/imcc предполагается imcc
cd languages/imcc
make -s
cd -
imcc -o $1.pbc -d -O1 $1.pasm
echo "b runops_jit
r -d -j $1.pbc
n
n
n
n
n
n
add-symbol-file $1.o 0
s
" > .ddd
ddd --command .ddd parrot &
Запускаем, например, с dddp t/op/jit_2, затем включаем статус регистров then turn on the register status, проходим по исходному файлу(step или nexti) или устанавливаем точки останова как и в любом другом языке. Хотя пока нет информации о номерах строк, вы можете удалять пустые строки и объединять метки с кодами операций в pasm-файле.
Вы можете проверять регистры Паррота через отладчик или даже устанавливать их и всегда можете заходить во внешний код операции и смотреть на *interpreter.
Тесты t/op/jit*.t имеют несколько тестовых ситуаций для тестирования назначения регистров. Эти тесты написаны для отображения на 4-х регистровый процессор. Если ваш процессор имеет больше четырех регистров, уменьшите значение до четырех и запустите тесты.
$ cat j.pasm
set I0, 10
set N1, 1.1
set S2, "abc"
print "\n"
end
$ dddp j
(ddd показывает исходный и ассемблерный код (начальный код пропущен):
0x815de46 <jit_func+30>: mov $0xa,%ebx
0x815de4b <jit_func+35>: fldl 0x81584c0
0x815de51 <jit_func+41>: fstp %st(2)
0x815de53 <jit_func+43>: mov %ebx,0x8158098
0x815de59 <jit_func+49>: fld %st(1)
0x815de5b <jit_func+51>: fstpl 0x8158120
0x815de61 <jit_func+57>: push $0x815cd90
0x815de66 <jit_func+62>: call 0x804db90 <Parrot_set_s_sc>
0x815de6b <jit_func+67>: add $0x4,%esp
0x815de6e <jit_func+70>: push $0x815cd9c
0x815de73 <jit_func+75>: call 0x804bcd0 <Parrot_print_sc>
0x815de78 <jit_func+80>: add $0x4,%esp
0x815de7b <jit_func+83>: add $0x4,%esp
0x815de81 <jit_func+89>: pop %edi
0x815de83 <jit_func+91>: pop %ebx
0x815de85 <jit_func+93>: pop %esi
0x815de87 <jit_func+95>: pop %ebp
0x815de89 <jit_func+97>: ret
(gdb) n
(gdb) n
(gdb) n
(gdb) p I0
$1 = 10
(gdb) p N1
$2 = 1.1000000000000001
(gdb) p *S2
$3 = {bufstart = 0x815ad30, buflen = 15, flags = 336128, bufused =
3, strstart = 0x815ad30 "abc"}
(gdb) p &I0
$4 = (INTVAL *) 0x8158098