Виртуализация исполнения машинного кода процессорной архитектуры ARM в Android-x86 окружении (1187396), страница 4
Текст из файла (страница 4)
Начиная с самогоглубокогоуровня,загрузчикпросматриваеттаблицурелокацийиудовлетворяет каждую из них.Механизм релокаций в современных системах используется дляудобного манипулирования библиотеками, а также возможностью связыватьисполняемые файлы и библиотеки друг с другом не на этапе сборки, а вовремя запуска, что значительным образом сокращает объёмы запускаемыхпрограмм, так как более нет нужды заранее собирать исполняемый файл извсех использующихся в нём библиотек. Однако при таком подходепоявляется существенная проблема – если в коде используется, к примеру,внешняя функция, то становится невозможным на этапе компиляцииподставить в машинные инструкции корректный адрес, так как этот адрес неизвестен заранее.
Более того, в настоящее время ради экономии ресурсов24программы, как правило, не используют копии общесистемных библиотек(таких, как libc), потому что это так же влечёт за собой дополнительныерасходы памяти. Вместо этого операционная система предоставляет общийдоступктемсегментамбиблиотеки,которыенемогутбытьмодифицированы (сегмент кода), и предоставляет копию тех участков, чтомодифицированы могут быть (сегмент данных).
Однако это достигается не науровне загрузчика, а на уровне системы виртуальной памяти, и такой подходназывается Copy-on-Write [12].Таким образом при старте выполнения загрузчик должен найти всеадреса внешних вызовов и переменных в программе, и заменить их накорректные адреса из внешних библиотек. Возможность этого достигается засчёт применения релокаций – каждая релокация содержит информацию оместе, куда необходимо подставить адрес неизвестного символа, его имя, атакже формат, в котором следует подставить этот адрес.
В настоящее времясамымраспространеннымподходомявляетсятакназываемыйPIC(position-independent code), который подразумевает подстановку адресов не вабсолютных значениях, а в виде разницы между адресом и тем местом,откуда его будут вызывать. Это сделано для возможности безболезненногоперемещения сегментов кода по виртуальной памяти при необходимости.
Вэтой связи в данный момент вызов внешних функций в коде начинается сперехода в так называемый PLT-stub, находящийся в таблице связыванияпроцедур, откуда уже происходит переход по адресу, определенномурелокацией. Это было сделано для того, чтобы собрать все вызовы каждойвнешней функции в одном месте, что позволило сократить время разрешениязависимостей на старте программы. Также для экономии времениприменяется приём под названием lazy linking, когда релокация разрешается25непосредственно перед первым вызовом функции, однако эта оптимизация втекущей работе не была выполнена.После разрешения всех зависимостей необходимо передать управлениеэмулятору, сообщив также адрес функции main, являющейся стандартнойвходной точкой.
Для этого вместо таблицы сегментов подгружается таблицасекций, а в ней ищется символьная таблица (SHT_SYMTAB). Далее в нейпросматриваются все записи типа STT_FUNC и ищется необходимыйсимвол. Получив её смещение и зная, по какому виртуальному адресу бызагружен соответствующий сегмент можно определить адрес необходимойфункции. Закончив работу, загрузчик передаёт управление эмулятору, а тот всвою очередь начинает исполнение гостевого кода начиная с точки входа впрограмму5. Оптимизация эмуляцииПосле реализации загрузчика встаёт вопрос о возможных оптимизацияхполученной системы.
Учитывая, что несмотря на различные архитектурыгостевой и хостовой платформ, обе они управляются одной и той же ОСможно сделать вывод, что вместо эмуляции окружения (в данном случаебиблиотек, динамически связываемых с исполняемым файлом), можноиспользовать уже существующие в системе библиотеки.Это решение обладает следующими преимуществами:1. Можно использовать стандартные средства связывания динамическихбиблиотек, а также поиска необходимых символов в них. Это уменьшаетколичество ошибок, а также увеличивает скорость запуска исполняемогофайла.262. Благодаря использованию родных для системы библиотек сталовозможным исполнять все вызовы к ним нативно. Таким образомэмулируется только выполнение кода исполняемого файла, все внешниевызовы исполняются непосредственно процессором.Все вышеобозначенные цели были достигнуты путём модификацииреализованного загрузчика.
Основные изменение коснулись этапа поиска иразрешения зависимостей. Вместо рекурсивной линковки необходимыхбиблиотек загрузчик ищет в таблице соответствия файл, соответствующийнеобходимой гостевой библиотеке и подгружет его с помощью стандартнойфункции dlopen. Далее обработчик загруженного файла помещается в кэшдля предотвращения лишней повторной загрузки.Далее на этапе разрешения зависимостей при поиске виртуальногоадреса очередного необходимого символа загрузчик обращается в кэшдинамических библиотек, слинкованных с исходным исполняемым файлом, иищет необходимый символ (функция dlsym). Найденный адрес заносится вспециальную таблицу, а на место предполагаемой релокации помещаетсятокен, соответствующий индексу в таблице символов.
Так было сделано длятого, чтобы эмулятор исполняя очередную команду мог понять, когданеобходимо сделать нативный вызов.Во время обработки каждой машинной инструкции гостевого кодаэмулятор производит проверку на соответствие вида очередной инструкциипаттерну токена. В случае, если это так, производится переход в кодхостовый код соответствующей функции.Рассмотрим подробнее нативный вызов из эмулируемого кода и27передачу аргументов в него. Проблема в данном случае состоит в том, чтосуществуют разные соглашения о вызове (так называемые calling conventions)для различных архитектур.
Так в случае x86 при 32-х битной адресации всепараметры передаются через стек, справа-налево (то есть первый аргументвсегда оказывается на вершине стека) [13,14,15]. Так было сделано дляудобства реализации функций с переменным числом аргументов (напримерprintf из стандартной библиотеки С). В случае же архитектуры ARM первые 4аргумента передаются через регистрые r0-r1, а остальные – таким же образомчерез стек. Так как в обоих случаях конвенция вызовов предполагает, чтостек очищает вызывающая функция, то нет нужды беспокоиться околичестве передаваемых аргументов. На данном этапе реализацииэмулятора во внешнюю библиотеку всегда передаётся 8 аргументов.Таким образом, каждый раз, когда эмулятор встречает токен на местеочередной машинной инструкции, происходит выход в функцию нативноговызова, которая, в свою очередь по токену определяет адрес библиотечнойфункции, которую необходимо вызвать.
Далее происходит процедурапередачи пареметров через реальный стек. Для этого в буфер копируютсязначения первых 4-х виртуальных регистров, содержащихся в vcpu, а затемчерез указатель на виртуальный стек, находящийся там же происходиткопирование в буфер ещё 4-х аргументов. После этого Весь буферпомещается в стек и происходит вызов внешней библиотечной функции.Затем значения регистров EAX и EBX, в которых, согласно соглашению овызовах, лежат возвращаемые значения вызванной функции копируются ввиртуальные регистры r0 и r1 соответственно.
Происходит очищениереального стека. Таким образом на выходе из функции нативного вызова всоответствующих регистрах архитектуры ARM находится корректноевозвращаемое значение, полученное после проброса библиотечной функции28в хостовую систему.Стоит заметить, что предлагаемая оптимизация использует тот факт, чтоAndroid-приложения используют JNI как правило именно для вызововнативных библиотек. Таким образом потери производительности, связанныес эмуляцией в реальных приложениях будут несущественными, посколькусам код библиотеки, как было показано, будет исполняться непосредственнона процессоре.6. Оценка быстродействия эмуляцииВажнойчастьюпредставленнойработыявляетсяоценкапроизводительности предлагаемого подхода. Эта характеристика особенноважна для проверки жизнеспособности предлагаемого подхода в реальныхусловиях.Для оценки скорости эмуляции машинных инструкций был выбрансинтетический тест – вычисление n-го числа Фибоначчи рекурсивнымметодом.
Такой алгоритм активно использует стек, вызовы функций, а такжецелочисленную арифметику. На Рис. 3 представлен график зависимостилогарифма времени вычисления n первых чисел Фибоначчи в зависимости отn.29Рис. 3. График времени исполнения программы эмулятором и QEMU.Кол-воФибоначчичисел Времяэмулятора, сработы Время работы QEMU, с50.1520.027100.1550.034150.1610.036200.2020.042250.5940.051271.2950.082281.9750.114293.0910.151304.9370.229317.8680.3513212.60.5483320.4370.863303434.8751.3413561.2632.208Табл. 1. Время исполнения программы эмулятором и QEMU.Из Табл. 1 видно, что в среднем эмулятор проигрывает QEMUпримерно 1-2 порядка при исполнении машинного кода без библиотечныхвызовов.