WindЦитата:
Цитата:
Ну если там вероятно находится "не код", значит он вероятно и не будет исполняться. Но это - вероятно, тем более, что откомпилить несколько лишних инструкций не займет много времени. Ты больше потратишь времени, на вычисление переходов внутри группы, чтобы выяснить действительно-ли вот этот переход - последний.
Цитата:
Цитата:
Блин вначале не допер.. Можно завести доп. таблицу переходов внутри группы, а можно сделать ещё одну look-up таблицу для сопоставления физического адреса и отрекомпиленного кода. Если переход локальный - внутри группы, то юзать локальную таблицу и пропатчить переходы после рекомпиляции всей группы. В общем случае - ссылка на глобальную таблицу, где также стоят STUB по умолчанию. Но я в деталях не стал рассматривать
Это уже у кого как получится спрограммировать..
Это дело другое, правда несет дополнительные издержки на память, но это не столь важно.
Только я все же за компиляцию до инструкции перехода, а в случае если переход внутри(или внутрь) блока, на еще нескомпилированую позицию, то просто продолжить компиляцию. При использовании этого подхода не теряешь время на иследование кода и впустую не компилируешь ненужный код.
RomikB Давно не был на форуме, так что начну своё возвращение с описания своего динамического рекомпилятора SonyPSX. R3000A(MIPS) в x86.
Способ очень простой, легко управляемый, без серьезных проблем в дебаге. Но написан на асме.
Главная мысль: Каждой 4-х байтной инструкции MIPS ставится в соответствии 16 байтная последовательность иструкций х86.
Таким образом любому физическому адресу в MIPS (mips_addr) соответствует адрес в x86 x86_addr = x86_base + mips_addr * 4.
Памяти в PSX 2Мб, таким образом рекомпилятор занимает 2Мб данные PSX плюс 2*4=8Мб, плюс 2, 4 или 8 Мб (память переходов в зависимости от реализации), всего 12, 14 или 18Мб.
План:
1) Переход на выполнение следующей инструкции.
а) если декодирована, то выполняеться (1)
б) если не декодирована то запускаеться "заглушка" код которой записан по этому адресу(2)
2) декодирование иструкции в 16 байтную последовательность
а) если простая инструкция то декодирование следующей инструкции(2)
б) если инструкция перехода то декодирование перехода(3)
3) декодирование инструкции перехода и следующий за ней инструкции (т.к. в MIPS инструкция перехода выполняеться вместе со следующей инструкцией.)
4) К пункту 1.
Рассмотрим пункт основной пункт 3:
У нас есть 2 инструкции. Мы получаем после декодирования 32 байта в памяти рекомпилятора (на 1 инструкцию 16 байт) и ещё 32, 40 или 48 байт в памяти переходов которые распределяються следующим образом.
Память рекомпилятора:
(16)Необходимые динамические вычисления (расчет адресов, проверка условия) и переход в память переходов (если надо).
(16)Декодированная спареная к переходу инструкция.
Память переходов:
(8,16)Сохранение адреса возврата(опционально)
(16)Декодированная спареная к переходу инструкция.
(8) вызов функции тайминга
(8) переход.
Я понимаю что всё муторно и непонятно, поэтому рассмотрим теперь всё по порядку.
Инициализируем регистры, память.
Заполняем память рекомпилятора заглушками (вызовами функции рекомпиляции)
Загружаем в память PSX исполняемый файл.
Определяем точку входа mips_start_addr и передаем управление x86_start_addr = x86_base + mips_start_addr * 4
Инструкция не декодирована, поэтому вызываеться "заглушка" и декодируеться блок инструкций до первого перехода.
Выполняеться блок инструкций.
Если переход без условный или условный с выполнением условия то запускаеться фунция тайминга и затем перезход
Декодировани или выполнение следующего блока.
Я думаю всё равно не понятно так что жду вопросов, если смогу - отвечу.
Маленький светловолосый Зайка...
Wind В ближайщее время тоже распишу свой вариант динамческой рекомпиляции.
RomikB Цитата:
Wind писал:
В ближайщее время тоже распишу свой вариант динамческой рекомпиляции.
С сорцами?
WindЦитата:
RomikB писал:
С сорцами?
Да мне жалко могу и сырцами.
RomikB Цитата:
Wind писал:
RomikB писал:
С сорцами?
Да мне жалко могу и сырцами.
Ну раз не жалко то давай. (Буду воровать твои идеи). =)))
WindИтак, предлагаю свой вариант построения динамической рекомпиляции.(Из sh4 в х86).
Сразу хочу предупредить эта статья, требует серьезных знаний в области программирования.
Перед запуском компилятора необходимо произвести некоторые подготовительные действия, они сведены в следующую функцию:
Код:
u8 *Rptr;
u8 *RMem;
u8 *RRom;
u8 *RRam;
u32 *RGetAddr;
typedef struct
{
u32 x86reg;
u32 sh4reg;
u32 old;
} _x86regs;
_x86regs x86regs[6];
void RInit()
{
u32 i, r;
/*
Выделяем память.
RRom – в этот участок памяти в последующем будут записываться адреса рекомпилированых кусков кода, находящихся в БИОСе.
RRam – в этот участок памяти в последующем будут записываться адреса рекомпилированых кусков кода, находящихся в оперативной памяти.
RMem – в этот участок памяти в последующем будут записываться рекомпилированый код.
RGetAddr – этот участок необходим для построения LOOK-UP таблиц.
*/
RRom = (u8*) calloc(0x00200000, 2);
RRam = (u8*) calloc(0x01000000, 2);
RMem = (u8*) calloc(0x01000000, 1);
RGetAddr= (u32*)calloc(0x00400000, 1);
/*
Строятся LOOK-UP таблицы.
*/
for (i=0; i<0x00200; i++) RGetAddr[i + 0x00000] = (u32)&RRom[i << 13];
for (i=0; i<0x00200; i++) RGetAddr[i + 0x80000] = (u32)&RRom[i << 13];
for (i=0; i<0x00200; i++) RGetAddr[i + 0xA0000] = (u32)&RRom[i << 13];
for (i=0; i<0x01000; i++) RGetAddr[i + 0x0c000] = (u32)&RRam[i << 13];
for (i=0; i<0x01000; i++) RGetAddr[i + 0x8c000] = (u32)&RRam[i << 13];
for (i=0; i<0x01000; i++) RGetAddr[i + 0xAc000] = (u32)&RRam[i << 13];
Rptr = RMem;
/*
Инициализируем структуру x86regs, необходимо для работы «регкешинга»
x86reg – это поле содержит номер регистра х86
sh4reg – это поле содержит номер регистра sh4
old - это поле содержит «время» последнего обращения к регистру
Регистры EAX и ESP в «регкешинге» не участвуют, первый используется для хранения временных данных, а второй должен указывать на стек.
*/
for(i = 0, r = 7; i < 6; i++, r--)
{
if(r == 4) r--;
x86regs[i].x86reg = r;
x86regs[i].sh4reg = -1;
x86regs[i].old = 0;
}
}
Работа компилятора начинается с вызова следующей функции:
Код:
typedef void (*RECFUNC)();
void RExecute()
{
for(;;)
{
/*
В переменную p записываем адрес начала 4кб сегмента.
«Зануляем» переменную recExecute.
*/
RECFUNC *p = (RECFUNC*)RGetAddr[sh4.pc >> 12];
RECFUNC recExecute = NULL;
/*
Если p не равно нулю, то в recExecute записываем адрес начала рекомпилированого кода, адрес считывается по смещению внутри 4кб сегмента.
Если p равно нулю, то значит произошла ошибка и необходимо завершить работу эмулятора.
*/
if(p)
{
recExecute = *(RECFUNC*)&p[(sh4.pc >> 1) & 0x07ff];
}
else
{
RErorr();
}
/*
Если recExecute содержит NULL, то значит этот участок еще не скомпилирован, вызывается функция компиляции, ее работа будет рассмотрена ниже. По окончании компиляции в recExecute будет записан адрес начала скомпилированного кода.
*/
if(recExecute == NULL)
{
Rcompile();
recExecute = *(RECFUNC*)&p[(sh4.pc >> 1) & 0x07ff];
}
/*
Исполняем скомпилированный код.
*/
recExecute();
}
}
Рассмотрим работу компилятора:
Код:
void Rcompile()
{
u32 i;
/*
В переменную p = записываем адрес начала 4кб сегмента.
*/
RECFUNC *p = (RECFUNC*)RGetAddr[sh4.pc >> 12];
/*
Если памяти для компиляции осталось мало, то «зануляем» всю память и заворачиваем указатель на код на начало.
*/
if(((u32)Rptr - (u32)RMem) >= (0x01000000 - 0x8000))
{
memset(RRom, 0, 0x00200000 * sizeof(u32));
memset(RRam, 0, 0x01000000 * sizeof(u32));
memset(RMem, 0, 0x01000000 * sizeof(u8 ));
Rptr = RMem;
}
/*
Выравниваем указатель на 32-ную границу и записываем его значение по смещению внутри 4кб сегмента.
*/
x86ptr = Rptr;
align(x86ptr, 32);
p[(sh4.pc >> 1) & 0x07ff] = (RECFUNC)x86ptr;
pc = sh4.pc;
time = 0;
loadcw = 0;
sh4.branch = 0;
cycle = 0;
/*
Компилируем блок размером не более 4кб(одна инструкция sh4 это 2 байта).
*/
for(i = 0; i < 2048; i++)
{
sh4.code = memRead16(pc);
ROpcodeTableAll[(sh4.code >> 12) & 0xf]();
pc += 2;
if(sh4.branch) break;
}
if(!sh4.branch)
MOV32ItoM((u32)&sh4.pc, pc + 2);
FlushAll();
LoadCW();
ADD32ItoM((u32)&sh4.cycle, cycle);
CALL32((u32)&CpuTest);
RET();
Rptr = x86ptr;
}
Рассмотрим пример компиляции инструкции:
Код:
void RMOVI()
{
/*
Функция GetX86reg возвращает номер свободного регистра х86 или регистра который уже сопоставлен значению _Rn_.
*/
x86Reg32Type Rn = GetX86reg(_Rn_, ModeWrite);
/*
В зависимости от значения _ImmS_ компилируем необходимый код.
*/
switch(_ImmS_)
{
case 0:
XOR32RtoR(Rn, Rn);
break;
case 1:
XOR32RtoR(Rn, Rn);
INC32R(Rn);
break;
case -1:
XOR32RtoR(Rn, Rn);
DEC32R(Rn);
break;
default:
XOR32RtoR(Rn, Rn);
OR32I8toR(Rn, _ImmS_);
break;
}
/*
Увеличиваем счетчик тактов
*/
cycle++;
}
На этом пока все, задавайте ваши вопросы.
RomikB 1) Что то не видно как ты используешь кэширование регистров
2) Поразительное сходство с исходниками PCSX.
Маленький светловолосый Зайка...
WindЦитата:
RomikB писал:
1) Что то не видно как ты используешь кэширование регистров
2) Поразительное сходство с исходниками PCSX.
Я же написал GetX86reg возвращает регистр, его номер это и есть регкешинг. На счет сходства это ты зря, им до меня как...
RomikB Угу, ты один переплюнул, группу разроботчиков =)
В связи с тем что я не знаком с устройством sh4, то вопросов нету.
Давай выпускай эмуль рулезный!
Wind Я сейсас не в состоянии над ним работать, времени нет вообще хочешь могу передать все сырцы, биос пускающие и ты можешь продолжить работу над ним
RomikBЦитата:
Wind писал:
Я сейсас не в состоянии над ним работать, времени нет вообще хочешь могу передать все сырцы, биос пускающие и ты можешь продолжить работу над ним
Большое спасибо за доверие, тока вот у меня свой эмулятор почти готовый есть и я вот тоже думаю кому бы сорцы передать чтобы он его закончил.
Wind Вот и я не могу сейчас никак продолжить работу, мне лично необходимо добавить работу с CD-ROM(там некое подобие ATAPI протокола), а дальше видно будет. Ты же над playstation работаешь у тебя хоть плугины есть а мне и графику приходится самому. Вообщем один я буду еще долго заканчивать.
RomikB Динамический Рекомпилер с предварительным анализом.
Допущения:
1) Каждая функция имеет только одну точку входа.
2) Адрес точки входа - нижняя граница всех адресов функции.
3) Каждая функция имеет конечное число точек выхода.
4) Каждая функция не являеться самомодифицируемой.
5) Точка выхода не может быть условной.
Идея:
Рекомпиляция проходит по функциям.
Рекомпиляция функции включает рекомпияцию всех адресов начиная от точки входа до всех точек выхода, включая локальные переходы, но исключая внешние вызовы.
Код функции при помощи точки входа, точек выхода, точек локального перехода, и точек куда ведут локальные переходы, делиться на блоки - минимальные единицы рекомпиляции.
Тайминг:
Считаеться для всего блока в сумме. Проверка на события в конце блока(возможно только в тех блоках которые заканчиваються локальным переходом, вызовом, или выходом).
Реализация:
Начиная с точки входа проходим по всей функции и создаем список адресов обычных адресов, адресов локального перехода и адресов куда ведут локальные переходы. Таким образом мы получаем блоки. Затем рекомпилируем их начиная с первого. Каждый блок отдельно оптимизируеться (N-проходный рекомпилер), и в конце ставиться прирост тайминга.
Как вариант возможна рекомпиляция блоков на лету ( а не вся функция ), с предварительным анализом всей функции.
Отличия:
Максимальльная оптимизация при минимальном обьеме.
Колинг:
Большинство аспектов знаю как реализовать легко и изящьно, другие муторно, а часть пока не обдумал.
RomikB Вопросы коментарии будут? А то начну реализовывать и окажеться что я ошибся