Виртуализация исполнения машинного кода процессорной архитектуры ARM в Android-x86 окружении (1187396), страница 7
Текст из файла (страница 7)
Единственная сложность в данном случае состоит вветвлении, статистический результат которого на эта CFA предсказатьневозможно. Таким образом в данном место сложно добавить какую-тооптимизацию, связанну/ с бинарной трансляцией. В конечном итогеполучаем:A->next_block == B; A->branch_block == NULLПоследним случаем является безусловная инструкция ветвления. Дляблоков,которыесформироватьещёзаканчиваютсяодинтакойCFA-блок,инструкциейначинающийсянеобходимосадреса,соответствующего целевому адресу инструкции безусловного перехода.Тогда для изначального блока получаем:46A->next_block == NULL; A->branch_block == BЗаметим, что в текущей реализации модуля необходимо задать точкувхода в бинарный файл, то есть адрес, с которого необходимо начинатьанализ потока исполнения машинного кода.
Однако разрешено указыватьнесколько точек входа и расширять объем проанализированного кода,потому что в случае разделяемых библиотек как правило имеется несколькоточек входа в машинный код ELF-файла. В этом случае необходимо послеэтапа загрузки взять адреса внешних символов разделяемой библиотеки,которые участвовали в релокациях и запустить анализ кода начиная с этихадресов. В конечном итоге на выходе из описываемого модуля мы получаемдревовидную структуру данных, которая хранит в себе полную информациюо ходе исполнения программы.Данный подход не покрывает случаи, когда к примеру REGPC грузитсянекоторый адрес машинной инструкции, который вычисляется динамическив ходе работы нативного кода. В этом случае возможны два подхода:1. Выходить в эмулятор и эмулировать исполнение нативного кодавплоть до ближайшего блока оттранслированного кода, далеевозвращаться к исполнению нативного кода.2.
Динамически в момент наступления ситуации, описанной вышевызывать последовательно всю процедуру бинарной трансляцииначиная с целевого адреса перехода. такой подход к сожалению чреватнепредвиденными задержками в работе нативного кода, так какпроцедурабинарнойтрансляциисоднойсторонывесьматребовательна к производительности, а с другой стороны активнообращается к различным разрозненным структурам в памяти,необходимым для корректного функционирования.
Всё это непозволяет эффективным образом использовать кеш процессора, что47делает весь участок кода, связанный с бинарной трансляцией весьмаресурсоемким.В конечном счёте в текущей реализации был выбран первый подход,так как он позволяет довольно гибко поступать в случае встречинеоттранслированного кода с одной стороны, и при этом ничто не мешает вдальнейшем расширить систему на второй случай, который сулит гораздобольшую производительность, хотя и может вносить непредвиденныезадержки.7.3 Анализ использования регистров и регистровый аллокаторВ целях оптимизации производительности в текущей реализациибинарной трансляции было принято решение использовать нативные х86регистры в качестве основного инструмента манипуляции даннымивиртуальногоконтекстапроцессора.Первоначальнопредлагалосьиспользовать для этих целей уже существующую и описанную вышеструктуру VCPU, которая является одной из основных частей эмулятора.Таким образом в любой момент при исполнении оттранслированного кодапредлагалосьподдерживатьконсистентноесостояниевиртуальногоконтекста, что позволяло в любой момент переключаться на исполнение кодас помощью эмулятора.
Однако при подобном подходе будет происходитьпостоянное обращение к оперативной памяти. Даже при условии нахожденияданных в кэше время обращения к ним существенно выше, чем к нативнымрегистрам. Также, учитывая что оттранслированный код активно обращаетсяк памяти довольна вероятна ситуация, когда виртуальный контекст будетнаходиться не в кэше верхнего уровня, что драматическим образом вноситзадержки на каждом обращении к нему [20,21,22].48После проведения замеров производительности, был сделан вывод, чтотакой первоначальный подход вносит до двух порядков задержки повремени. На каждый такт исполнения нативной инструкции процессорпростаивал бы несколько десятков тактов при обращении к кэшу второгоуровня (или к оперативной памяти в худшем случае).
В конечном счете былоприняторешениеиспользоватьнативныерегистрыипопытатьсяоттранслировать ARM инструкции как можно ближе семантически винструкции x86.Самой большой проблемой в данном случае помимо сложности иархитектурного несоответствия между двумя разнородными наборамиинструкций является нехватка х86 регистров по сравнению с ARMсобратьями. Заметим, что в данном случае среди хостовых регистров мыможем свободно использовать лишь 6 (EAX, EBX, ECX, EDX, ESI, EDI).Стековый регистр ESP необходимо сохранить в нетронутом состоянии, таккак стек необходим для выполнения как оттранслированного кода, так и кодаэмулятора, а ещё один регистр используется для хранения указателя навиртуальный контекст процессора, на случай особых случаев, когда всё жетребуется выйти в эмулятор.В конечном итоге для построения соответствия между ARMрегистрами и x86 регистрами был реализован модуль, действующий подобнорегистровому аллокатору в компиляторе [16,17].
Для каждого блока кодаопкоды машинных инструкций анализируются с точки зрения использованияими регистров. Далее строится граф использования регистров, при этомразделяются случаи, когда регистры используются для чтения или записи.Далее данный граф максимально глубоко “раскрашивается” в доступные49шесть цветов (по количеству доступных регистров).
Таким образом для блокакода строится максимальная последовательность инструкций, которые могутбыть исполнены с использованием нативных регистров. Далее, в случае еслибыл покрашен не весь граф, пройденные вершины отсекаются, а процедурапокраски начинается заново.
Заметим, что в данном случае мы применяем посути вид жадного алгоритма, когда на каждом этапе покраски мы пытаемсяпокрасить максимальный подграф. Учитывая, что машинный код ARMизначально попадает к нам будучи скомпилированным с помощью NDK, этотподход будет работать эффективно, так как компилятор gcc изначальностарается разложить код при компиляции по нативным регистрам наиболееэффективным образом [17,18].
В результате использование жадногоалгоритма позволило решить эту задачу эффективно как с точки зренияконечного результата, так и с точки зрения асимптотической оценки повремени (линейно).Остановимся чуть подробнее на том, каким образом анализируетсяиспользование регистров для каждого опкода. В данном случае механизмработы схож с декодированием. Для каждой инструкции или схожего набораинструкций в таблице декодирования хранится указатель на функцию,которая заполняет соответствующую структуру данных, используемую дляхранения информации об использовании регистров. Учитывая, что вархитектуре ARM есть не так уж и много различного рода видов инструкцийможно эффективным образом разделить весь набор опкодов на классы,которые будут анализироваться конкретным образом.
Это позволяет с однойстороны упростить код, а с другой стороны сократить количество вызовов,необходимое для анализа использования регистров. Заметим, что в целяхсохранения модульности и взаимозаменяемости данный код существует отдальнейшего этапа, которым и является регистровый аллокатор.50На выходе из данного модуля для каждого блока кода мы получаемнекоторую последовательность соответствий регистров ARM-x86, которуюможно применять в дальнейшем при трансляции.7.4 Модуль бинарной трансляцииВ данной главе будет механизм динамической бинарной трансляции,который позволил существенным образом ускорить выполнение машинногокода ARM целевого исполняемого файла. На момент написания даннойработы в текущей версии динамического транслятора реализован весь наборинструкций набора команд ARM уровня пользователя, а также ведетсяработа по имплементации набора инструкций THUMB.
Оценочно можносказать что трансляцию этого укороченного набора инструкций можно будетреализовать существенно проще, так как он включает в себя меньшееколичество опкодов, а также из за сокращенного опкода обладает в среднемболее простым поведением в расчете на инструкцию, что упрощаеттрансляцию.Напомним в общих чертах какая задача стояла перед началомреализации динамической бинарной трансляции машинного кода ARM внабор инструкций архитектуры IA-32. В первую очередь стоит упомянуть,что на момент начала разработки уже были проведены эксперименты сиспользованием нативных x86 регистров вместо постоянных обращений квиртуальному контексту процессора, поэтому окончательный варианталгоритма трансляции осуществлялся сразу с прицелом на даннуюоптимизацию и в расчете на максимальную производительность. В конечномсчете за счет модуля анализа потока исполнения кода (CFA) задачу удалось51удачным образом декомпозировать на подзадачи гораздо меньшего размера(непрерывные блоки кода), которые гораздо проще было формализовать иоптимизировать.
На входе транслятору подаётся не просто описатель блокакода, а специализированная структура данных, детально описывающаяповедение машинных инструкций. Рассмотри представление данных притрансляции более подробно. После стадии CFA и регистрового аллокатора навход алгоритму трансляции последовательно подаются на вход описателиблоков кода в удобной форме, и далее происходит работа с ними.typedef struct {stub_chunk_t* head_chunk;uint8_t *stub_addr;uint8_t *stub_tail_addr;x86_reg_t in_reg_map[ARM_REG_COUNT];x86_reg_t out_reg_map[ARM_REG_COUNT];trans_insn_t *branch_insn;trans_insn_t *insn_list_head;trans_insn_t *insn_list_tail;} translated_block_t;В данном случае стоит оговориться, что так как трансляция происходитв среде процессорной архитектуры IA-32, это налагает определенныеограничения.