Skip to content

QEMU, Быстрый Переносимый Динамический Транслятор

Anton Mitrokhin edited this page May 16, 2016 · 4 revisions
Фабрис Беллар

Краткий обзор

В данной работе мы хотим продемонстрировать QEMU – быстрый эмулятор аппаратного обеспечения, созданный на основе написанного нами переносимого динамического транслятора. QEMU способен эмулировать несколько архитектур процессоров (x86, PowerPC, ARM и Sparc) на различных платформах (x86, PowerPC, ARM, Sparc, Alpha и MIPS). QEMU поддерживает как полную эмуляцию системы, когда в виртуальной среде запускается немодифицированная версия операционной системы, так и режим эмуляции Linux процесса, в котором можно запустить на целевой архитектуре только один процесс.

1. Введение

QEMU является эмулятором аппаратного обеспечения – он способен запустить немодифицированную операционную систему (такую как Windows или Linux) и все их встроенные приложения в виртуальной среде. В свою очередь, QEMU может быть запущен на нескольких операционных системах, таких как Linux, Windows или Mac OS X. Архитектуры целевого процессора и процессора – носителя могут быть различными. Основной целью использования QEMU является запуск одной операционной системы внутри другой, например Windows внутри Linux, или наоборот. Ещё одно преимущество использования QEMU – возможность приостановить, сохранить или восстановить исполнение виртуальной среды, что может облегчаить отладку программного обеспечения. QEMU также включает в себя режим эмуляци отдельных процессов. Этот режим позволяет запускать Linux-процессы для целевого процессара на другом процессоре и в основном используется для тестирования кросс-компиляторов или эмуляторов процессора не запуская виртуальную машину полностью. QEMU состоит из нескольких подсистем:

  • Эмулятора ЦПУ (на текущий момент x861, PowerPC, ARM и Sparc)
  • Эмулятора устройств (напр. VGA дисплея, 16450 последовательного порта, PS/2 мыши и клавиатуры, IDE жёсткого диска, сетевой карты NE2000, ...)
  • Специальных устройств (напр. блочных устройств, символьных устройств, сетевых устройств), используемых для соединения физических устройств с эмулируемыми
  • Описания аппаратуры (напр. PC, PowerMac, Sun4m), инстанциирующего эмулируемое устройство
  • Отладчика
  • Пользовательского интерфейса

Эта статья рассматривает устройство динамического транстятора, используемого QEMU. Динамический транслятор выполняет преобразование инструкций целевой архитектуры в набор инструкций процессора-носителя в реальном времени. Получающийся в результате бинарный код сохраняется в трансляционном кеше с целью дальнейшего переиспользования. Преимущество по сравнению с интерпретаторами заключается в том, что целевые инструкции загружаются и декодируются долько один раз. Как правило, динамические трансляторы тяжело переносить с одной архитектуры-носителя на другую, так как приходится полностью переписывать кодогенератор. Эта задача, сравнимая по сложности с добавлением новой цели компиляции для компилятора языка C. QEMU значительно проще, так как он работает за счёт простой конкатенации участков машинного кода, сгенерированного заранее компилятором C [5]. Для эмуляции процессора необходимо также решить множество других, более классических, но не менее сложных [2] задач:

  • Управление кешом оттранслированного кода
  • Размещение регистров
  • Оптимизация кода
  • Связывание блоков
  • Управление памятью
  • Поддержка самомодифицирующегося кода
  • Поддержка исключений
  • Аппаратные прерывания
  • Эмуляция процессов UML

2. Динамическая трансляция с возможностью переносимости

2.1 Описание

Первым шагом является деление каждой инструкции целевого процессора на несколько более простых инструкций, называемых микро операциями. Каждая такая микро операция реализована посредством небольшой программы на языке C. Эта программа компилируется GCC в объектный файл. Микро операции выбираются таким образом, чтобы их число было значительно меньше (обычно, на несколько сотен), чем количество комбинаций всех инструкций и операндов целевой архитектуры. Трансляция с инструкций целевой архитектуры в микро операции реализуется кодом, написанным вручную. Исходный код оптимизируется для увеличения читаемости и компактности, так как скорость на этом этапе менее критична, чем в интерпретаторе. Утилита времени компиляции dyngen использует полученный ранее объектный файл с микро операциями чтобы сгенерировать динамический кодогенератор. Созданный кодогенератор вызывается во время работы программы, чтобы сгенерировать функцию, которая состоит из нескольких сконкатенированных микро операций. Описанный процесс близок к [1], но здесь проводится больше работы во время компиляции, чтобы получить лучшую производительность. В частности, ключевой идеей является то, что в QEMU микро операциям можно передавать константные параметры. Для этой цели, для каждого константного параметра GCC генерирует пустые релокации кода. Это позволяет dyngen распознать релокации и создать корректный код на C, а затем разрешить их во время построения динамического кода. Релокации также позволяют поддержать ссылки на статические данные и на другие функции в микро операциях.

2.2 Пример

Рассмотрим пример, в котором необходимо транслировать следующую инструкцию PowerPC в код x86:

addi r1,r1,-16 # r1 = r1 - 16

Транслятор кода PowerPC сгенерирует такие микро операции:

movl_T0_r1 # T0 = r1
addl_T0_im -16 # T0 = T0 - 16
movl_r1_T0 # r1 = T0

Количество микро операций минимализировано, при том без серьёзных последствий для качества кода. Например, вместо генерации каждой возможной операции move между всеми возможными 32 регистрами PowerPC, мы просто генерируем move между временными регистрами. Эти регистры T0, T1, T2 как правило занимают хостовые регистры посредством статического регистрового расширения переменных GCC.

Код для микро операции movl T0 r1 обычно выглядит как:

void op_movl_T0_r1(void)
{
  T0 = env->regs[1];
}

env - структура, содержащая состояние целевого процессора. 32 регистра PowerPC хранятся в массиве

env->regs[32].

addl T0 im гораздо более интересный пример, так как он использует константный параметр, значение которого определено только в момент исполнения:

extern int __op_param1;
void op_addl_T0_im(void)
{
  T0 = T0 + ((long)(&__op_param1));
}

Кодогенератор, созданный dyngen принимает поток микро операций, на которые указывает opc ptr и выводит хостовый код, на который указывает gen code ptr. На параметры микро операций указывает opparam ptr.

[...]
for(;;) {
  switch(*opc_ptr++) {
    [...]
    case INDEX_op_movl_T0_r1:
    {
      extern void op_movl_T0_r1();
      memcpy(gen_code_ptr, (char *)&op_movl_T0_r1+0, 3);
      gen_code_ptr += 3;
      break;
    }
    case INDEX_op_addl_T0_im:
    {
      long param1;
      extern void op_addl_T0_im();
      memcpy(gen_code_ptr, (char *)&op_addl_T0_im+0, 6);
      param1 = *opparam_ptr++;
      *(uint32_t *)(gen_code_ptr + 2) = param1;
      gen_code_ptr += 6;
      break;
    }
    [...]
  }
}

Для большинства микро операций, таких как movl T0 r1, хостовый код, сгенерированный GCC просто копируется. Когда используются константные параметры, dyngen использует тот факт, что GCC генерирует релокации к op papam1, чтобы выполнить распространение параметра времени исполнения (здесь он назван param1).

Когда запускается генератор кода, результатом будет следующий хостовый код:

# movl_T0_r1
# ebx = env->regs[1]
mov 0x4(%ebp),%ebx
# addl_T0_im -16
# ebx = ebx - 16
add $0xfffffff0,%ebx
# movl_r1_T0
# env->regs[1] = ebx
mov %ebx,0x4(%ebp)

На архитектуре x86, T0 привязан к регистру ebx, а контекст состояния процессора к ebp.

2.3 Имплементация Dyngen

Утилита dyngen является ключевым компонентом процесса трансляции в QEMU. При её запуске на объектном файле, содержащем микро операции, выполняются следующие задачи:

  • Из объектного файла извлекается таблица символов, его поле релокаций и секция кода. Этот этап зависит от формата хостового объектного файла (dyngen поддерживает ELF (Linux), PE-COFF (Windows) и MACH-O (Mac OS X)).

  • Микро операции размещаются в секции кода по таблице символов. Для определения начала и конца копируемого кода используется метод, специфичный для данного хоста. Как правило, пролог и эпилог функции пропускается.

  • Релокации каждой микро операции анализируются для того, чтобы извлечь число константных параметров. Константные релокации параметров определяются по наличию у них специфического имени op paramN.

  • Для копирования кода микро операции, в C генерируется копирование памяти. Данные о релокациях кода в каждой микро операции используются для того, чтобы корректно модифицировать скопированный код и выполнить релокации. Процесс модифицирования кода для релокаций специфичен для каждого хоста.

  • Для некоторых хостов, таких как ARM, константы должны располагаться рядом со сгенерированным кодом, так как доступы к этим константам осуществляются по относительным адресам с небольшим смещением. Для релокации констант используется машинно зависимый алгоритм. При компиляции кода микро операций используется набор GCC флагов для управления генерацией пролога и эпилога функций в такую форму, которую легко обрабатывать. Специальный ассемблерный макрос вынуждает GCC всегда завершать функции, соответствующие каждой микро операции, одиночной инструкцией return. Конкатенация кода не работала бы в случае, если бы в микро операции было бы несколько return инструкций.

3 Детали реализации

3.1 Оттранслированные блоки и кеш трансляции

Когда QEMU в первый раз встречает участок целевого кода, он транслирует его в хостовый код вплоть до следующей инструкции jump или инструкции, модифицирующей статическое состояние процессора так, что невозможно его определить в момент трансляции. Полученные блоки мы называем Блоками Трансляции (TBs).

16 мегабайтный кеш содержит наиболее недавно использованные блоки трансляции. Для простоты, кеш полностью освобождается при переполнении. Статическим состоянием процессора называется та часть состояния процессора, которая известна во время трансляции при входе в блок. Например, счётчик инструкций (PC) известен во время трансляции на всех архитектурах. На x86, статическое состояние процессора включает в себя больше данных для генерации более эффективного кода. Важно, например, знать, находится ли процессор в защищённом режиме или режиме реальных адресов, в пользовательском режиме или режиме ядра, и равна ли длина операнда по умолчанию 16 или 32 битам.

3.2 Распределение регистров

QEMU реализует фиксированное распределение регистров. Это значит, что каждый регистр целевого процессора привязан к конкретному хостовому регистру или адресу в памяти. На большинстве хостов, мы просто привязываем все целевые регистры к памяти и храним только несколько временных переменных в регистрах хоста. Распределение временных переменных жёстко закодировано в описании каждого целевого процессора. Преимуществом этого метода является простота и портируемость. Последующие версии QEMU будут использовать динамический аллокатор временных регистров с тем, чтобы избежать лишних операций с памятью в случае когда регистры цели могут быть прямо сохранены в регистры хоста.

Clone this wiki locally