Как эмуляторы транслируют клиентский код в машинный?

Представим такой упрощенный код, интерпретатора шитого кода.
Вопрос, на которой не могу найти ответа. Когда эмулятор собирает и компилирует код jit,
то как он этот код в машинных инструкциях составляет. Ведь если запустить такой код 1в1 соотношений инструкций, или более оптимизированный даже, есть прерывания, есть некие интерфейсы чью логику нужно поддерживать. (а невозможно выйти из выполнения клинского кода, если только в самом клиентском коде, не будет спец. инструкций переключения, проблема остановки наверное)
Ведь ему надо к примеру
Перед выполнением каждой инструкции проверять наличие прерывания
А значит ему нужно компилировать код, вставляя через инструкцию, инструкцию проверки выполнения условия, и инструкцию перехода. В общем минимум 3 инструкции.
Плюс там наверное, еще нужно поддерживать эмуляцию разных устройств.
На псевдокоде простой интерпретатор будет как-то так, куча проверок перед каждым выполнением,
но статистически известно, что прерывание вещь редкая, но требует мгновенной реакции(или вопрос на эмуляторах она не требует мгновенной реакции).
while(env->pc>0){
       if(env->IsInterrupt){
              pic->Interrupt();        
         }
          code[pc].Execute(param[pc]);
}

Я придумал, к примеру, вшить проверку прерываний, в некоторые инструкции. Я читал
while(env->pc>0){ 
          code[pc].Execute(param[pc]);
             // некоторые инструкции имеют в теле проверку прерываний
            // все инструкции перехода, деления
}

Я читал "Программное моделирование вычислительных систем Учебное пособие", там я я не нашел конкретный ответ, либо я его не понял.
Но там было типа написано, что типа надо проверять после каждой инструкции, что не оптимально, либо же делать Callback когда нужно, но без конкретики.
Еще вопрос из теории графов потока управлений итд.
При генерации блоков кода, этот код весь линеен, то есть все инструкции программы друг за другом выполняются? В таком случае, все в один массив записывается, но тогда, короче, после некоторых инструкций перехода, будет так, что инстурукция ниже будет к другому коду относится, и тогда у этой инструкции перехода будет if(cond) прыжок в одно место, else в другое,
вместо просто PC+=1(для примера выше)
Или же есть несколько блоков, где линейно выполняется весь блок, а последняя инструкция прыгнет в другой блок. Тогда можно представить в контексте примера, как несколько массивов code.

К примеру есть такой граф. Цифрами будет обозначен порядок прохождения
64e49c7f83816965297119.png
Если все строить в один последовательный массив, то будет построен такой код,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17. Из графа видно, что после блока 13 есть переходы на 9 или на 17, а не на след за ним 14, что усложняет. (я эту ошибку очень долго найти не мог, пока не понял)
Я читал, и вроде как делаются разные блоки, типа трассировки, и после проождения одного происходит переключение на другой(но не особо понял), к примеру для вот такого примера графа, сколько базовых блоков можно построить?
  • Вопрос задан
  • 363 просмотра
Решения вопроса 1
jcmvbkbc
@jcmvbkbc
"I'm here to consult you" © Dogbert
Когда эмулятор собирает и компилирует код jit, то как он этот код в машинных инструкциях составляет.

Если ты посмотришь на QEMU, то у него есть фронт-енды (https://github.com/qemu/qemu/tree/v8.1.0/target), каждый из которых транслирует инструкции эмулируемой машины в промежуточный код. И есть бэк-енды (https://github.com/qemu/qemu/tree/v8.1.0/tcg), каждый из которых транслирует инструкции промежуточного кода в инструкции хостовой машины. Каждая гостевая инструкция может превратиться во множество промежуточных, а каждая промежуточная -- во множество хостовых. У разработчиков есть правило, что если на гостевую инструкцию требуется больше 20 промежуточных, то вместо прямой трансляции такая инструкция реализуется как вызов функции на C. Инструкции транслируются базовыми блоками, с заданного адреса и до достижения одного из следующих условий: 1) встречена инструкция выполняющая переход (условный или безусловный, вызов функции, возврат из функции, сюда же относятся инструкции гарантированно вызывающие исключение), или 2) PC переходит через границу страницы виртуальной памяти, или 3) количество инструкций в базовом блоке превышает заданный предел. Вдобавок с каждым оттранслированным базовым блоком ассоциируется дополнительный набор флагов, определяемый фронт-ендом, который кодирует состояние, в котором была машина при трансляции этого кода. Это позволяет иметь несколько вариантов трансляции для кода начинающегося с одного и того же адреса, например для разных уровней привилегий. Оттранслированные базовые блоки помещаются в кеш с функцией поиска по комбинации адреса и дополнительного набора флагов. В цикле выполнения эмулятор ищет транслированный базовый блок кода в кеше (а если не находит его, то транслирует и помещает в кеш), запускает его и получает контроль после завершения его выполнения.

надо к примеру Перед выполнением каждой инструкции проверять наличие прерывания

Вовсе не каждой, даже в 100% точной эмуляции нужно проверять IRQ только когда прерывания разрешены. QEMU обычно проверяет запрос на прерывание только перед входом в оттранслированный базовый блок.

Или же есть несколько блоков, где линейно выполняется весь блок, а последняя инструкция прыгнет в другой блок.

Да, QEMU выполняет трансляцию базовыми блоками.

к примеру для вот такого примера графа, сколько базовых блоков можно построить?

В этом графе не обозначены безусловные переходы, если их нет, то QEMU мог бы выделить такие базовые блоки: 0-1-2-3, 4-5-6, 7-8-1-2-3, 9-10, 11-12-13, 14-15-16-2-3, 17, всего 7 блоков.
Если безусловные переходы -- это все переходы от узлов с бОльшими номерами к узлам с меньшими, то картина была бы такой: 0-1-2-3, 4-5-6, 7-8, 1-2-3, 9-10, 11-12-13, 14-15-16, 2-3, 17. Да, фрагмент 2-3 оттранслирован три раза: сам по себе и в составе других блоков.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@d-stream
Готовые решения - не подаю, но...
Наверное стоит заглянуть с другой стороны: "как интерпретатор работает" )
И собственно окажется что jit/il - интерпретируется некоей средой выполнения.
Тот самый il - максимально удобен для его интерпретации

Кстати подобные решения практикуются давно. Как образчик на моей памяти ca clipper 40-летней давности. Ну и всякие реализации кнутовской машины.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы