Эмулятор PS2 на Android — вторая серия

от автора

Привет всем читателям!

Я продолжаю публикацию по своему проекту портирования кода PCSX2 эмулятора PS2 на Android платформу.

Поспешу предупредить, что скачать и запустить не получиться — проект только на начальной стадии развития. Однако, для тех читателей, кто не лишён профессионального любопытства — добро пожаловать под кат. Что найдёт любопытствующий:

  1. компилируемый код для AARM64 — да, ядро PCSX2 эмулятора компилируется в нативный ARM код;

  2. исполняемое приложение для загрузки файлов БИСОа и образа игровых дисков;

  3. шок контент.

Что же, прогресс портирования зашёл достаточно далеко и получилось скомпилировать исполняемый нативный С++ код на AARM64. Средой разработки является Android Studio и при портировании кода с x86 на AARM64 я столкнулся с очевидной проблемой — различный набор инструкций процессоров. Многие удивятся — что за чушь, пиши на С++ и компилятор сам всё сделает. И здесь заключается сама суть проблемы портирования, с которой я столкнулся: PCSX2 создаёт исполняемый двоичный код процессора «на лету». Да, в коде эмулятора есть класс x86Emitter для записи в массив байтов байтовый код x86 процессора.

Так что же получилось?

Java frontend код для загрузки БИОСа и файлового образа игр. Пользовательский интерфейс прост и включает следующие окна:

Идея следующая — первоначально требуется выбрать файл БИОСа и файл образа диска для начала отладки кода. Полные пути к выбранным файлам сохраняются как параметры программы:

    public void save()     {         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(GlobalApplication.getAppContext());          SharedPreferences.Editor editor = preferences.edit();          try {              String l_value = serialize();              editor.putString(BIOS_INFO_COLLECTION, l_value);         } catch (ParserConfigurationException e) {             e.printStackTrace();         } catch (TransformerException e) {             e.printStackTrace();         }          editor.commit();     }

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

    private void autoLaunch()     {         BIOSAdapter.getInstance();          ISOAdapter.getInstance();          if(PCSX2Controller.getInstance().getBiosInfo() != null &&            PCSX2Controller.getInstance().getIsoInfo() != null)             GameController.getInstance().PlayPause();     }

Портирование компилятора кода эмулятора с x86 на AARM64 представляет серьёзную проблему. Архитектура Интелл относиться к CISC с переменной длинной кода и смешанной последовательностью данных и кода, в то время как AARM64 относиться RISC с фиксированной длинной кода в 32 бита. Но проблема в том, что на синтаксисе Интелловской архитектуре завязаны десятки и десятки файлов и сотни тысяч строк кода. Не говоря о том, что возникнет проблема в совместимости кода с оригинальным PCSX2 эмулятором. Что же, решение очевидное — написать оболочку x86 синтаксиса в исполнении AARM64 кода. Да, в моём проекте нет ничего оригинального — просто попытка эмуляции х86 кода через AARM64 код.

Конечно, я не ставлю целью закрыть всё множество х86 кодов — я поставил целью закрыть коды только используемых PCSX2 эмулятором. С этой целью в добавлен вызов нативного кода в момент создания приложения- PCSX2LibNative.getInstance().CPU_test()

       @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);          checkPermission();          setContentView(R.layout.activity_main);           Button l_controlBtn = findViewById(R.id.controlBtn);          if(l_controlBtn != null)         {            l_controlBtn.setOnClickListener(                        new View.OnClickListener(){                                                       @Override                                                       public void onClick(View v) {                                                           showControl();                                                       }                                                   }             );         }           PCSX2LibNative.getInstance().CPU_test();          autoLaunch();      } 

Что делает данная функция? Генерирует AARM64 исполняемый бинарный код для множества x86 кодов и проверяет результат исполнения с значением, известным из спецификации Интелл. Пример теста AARM64 кода для SUB комманды x86:

    void execute()     {          // Установка права доступа чтение/запись для области памяти         HostSys::MemProtectStatic(eeRecDispatchers, PageAccess_ReadWrite());          // Очистка области памяти         memset(eeRecDispatchers, 0xcc, __pagesize); 				         // Установка аргумента теста команды         s_stateTest = 0x10;          // Передача указателя на начало области памяти в генератор AARM64 кода         xSetPtr(eeRecDispatchers);          // Создание исполняемого кода в области памяти данных         auto DynGen_CodeSUB = _DynGen_CodeSUB();          // Установка права доступа в статус исполняемой области памяти         HostSys::MemProtectStatic(eeRecDispatchers, PageAccess_ExecOnly());          // Исполнение только что созданного AARM64 кода         auto l_result = CallPtr((void *)DynGen_CodeSUB);          // Проверка с ожидаемым результатом исполнения кода для x86!!!         if(l_result != 2147483664)         {             throw L"Unimplemented!!!";         }     }      DynGenFunc *_DynGen_CodeSUB()     {         // Указатель на начало исполняемой области памяти         u8 *retval = xGetAlignedCallTarget();          { // Properly scope the frame prologue/epilogue #ifdef ENABLE_VTUNE             xScopedStackFrame frame(true); #else             xScopedStackFrame frame(IsDevBuild); #endif              // Загрузка в регистр eax значения аргумента теста s_stateTest              // по эффективному адресу &s_stateTest              xMOV( eax, ptr[&s_stateTest] ); 			             // Исполнение команды x86 SUB с аргументом из регистра eax             // и прямо заданного аргумента 0x80000000             xSUB( eax, 0x80000000 );         }          // Выход из сгенерированной функции в вызывающую нативную функцию         xRET();          return (DynGenFunc *)retval;     }

Да, таких тестов исполнения эмуляции х86 кода в проекте множество — это и есть процесс разработки: исследование х86 команды и написание теста эмуляции на AARM64.

Шок контент!!!

При исследовании работы PCSX2 эмулятора я обратил внимание код компиляции исполнения команд процессор PS2 — R3000A:

static DynGenFunc* _DynGen_DispatcherReg() { 	u8* retval = xGetPtr(); 	   // Загрузка значения счётчика команд в регистр eax. 	xMOV( eax, ptr[&psxRegs.pc] );      // Копирование значения счётчика команд из регистра eax в регистр ebx. 	xMOV( ebx, eax );      // Получение относительного адреса из значения счётчика команд в регистре eax   	xSHR( eax, 16 );      // Получение адреса указателя массив указателей на начало блоков эмуляции команд R3000A 	xMOV( rcx, ptrNative[xComplexAddress(rcx, psxRecLUT, rax*wordsize)] );      // Переход по указателю на начало блока эмуляции команд R3000A 	xJMP( ptrNative[rbx*(wordsize/4) + rcx] ); 	return (DynGenFunc*)retval; }

Где psxRegs.pc — переменная для хранения значения счётчика команд процессора R3000A, psxRecLUT — указатель на массив указателей на скомпилированные R3000A команды. Схема работы кода имеет следующий вид:

И тут меня «ударило»!!!

Область памяти, указанная как исполняемая, включает в себя буквально несколько байт кода, но сгенерированная эмуляция команд R3000A сохраняется в обычной области данных и исполняется от туда! А контроль права исполнения операционной системы и процессора куда смотрит?

Для любителей повозиться с кодом — проект для среды разработки Android Studio доступен на GitHub: AndroidStudio

.

ссылка на оригинал статьи https://habr.com/ru/post/545392/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *