Виртуализация исполнения машинного кода процессорной архитектуры ARM в Android-x86 окружении (1187396), страница 3
Текст из файла (страница 3)
Эта информация хранится в CPSR регистре.При полной аппаратной эмуляции на этапе выборки также проверяется,возможно ли вообще чтение и выполнение кода с используемой страницыпамяти, однако в данной работе, как будет показано впоследствии, этотмомент является несущественным, по причине того, что вся эмуляцияпроизводится в user-mode.После выборки 2-х или 4-х байтная машинная инструкция попадает наэтапдекодирования.Декодерсопоставляетпаттернинструкциисфункцией-обработчиком, который эту инструкцию будет эмулировать.Рис. 2. Опкоды машинных инструкций ARM.В текущей реализации декодинг реализуется за счёт так называемыхтаблиц декодинга – многоуровневых таблиц, позволяющий находить нужныйобработчик за константное время. Это возможно благодаря тому, что вархитектуре ARM команды группируются по битовым маскам, что облегчаетпоиск (Рис. 2).17Далеедекодированнаямашиннаяинструкцияпопадаетвсоответствующую ей функцию-обработчик.
В общем случае эта функция:1. Разбирает аргументы инструкции.2. Эмулирует действие этой инструкции над vcpu.3. Опционально – устанавливает необходимые флаги, меняет режимпроцессора.4.2 User-mode эмуляцияТак как целью данной работы является предоставить возможностьисполненияAndroid-приложений,содержащихнативныйкод,скомпилированный под архитектуру ARM, на системах, базирующихся наархитектуре x86 под управлением Android, то целесообразно было неиспользовать описанный выше полный цикл эмуляции, а оставить из неголишь часть, непосредственно необходимую для выполнения задачи.ВэтойсвязибылорешеноотказатьсяотполнойэмуляцииAndroid-системы и использовать так называемую user-mode эмуляцию. Онаподразумевает применение технологий виртуализации на уровне отдельногоприложения, и без ненужной эмуляции устройств, памяти, сети, так как всёэто уже и так есть в хостовой системе.Таким образом задача существенно упрощается, так как можноограничиться узким кругом необходимых инструментов.
Например вместополного виртуального представления процессора осталось необходимымподдерживать лишь режим пользователя (user mode), так как именно в этомрежиме выполняются все прикладные программы. Это сократило количество18хранимых регистров, упростило операции с ними, так как больше нетребовалось контролировать работу перекрывающихся банков с регистрами.Также это позволило упростить операцию декодирования, сокративнеобходимый набор инструкций. Некоторые инструкции ARM могут бытьвыполнены только в привилегированном системном, супервизорном режимеили в режиме прерывания.
Очевидно, что все эти режимы не требуются,когда речь идёт о выполнения программного кода в режиме пользователя.Значительные изменение постигла также и подсистему памяти. Вместопрослойки, предоставляющей эмулятору честную систему виртуальнойпамяти было решено использовать виртуальную память хостовой ОС. Такимобразом для гостевого кода ничего не изменилось – он всё так же работает свиртуальной памятью. Однако теперь нет необходимости заботится овыделении/утилизации страниц, а также об обеспечении корректного доступак ним – вся эта работа выполняется хостовой ОС.4.3 ЗагрузчикЕщё одной крупной частью данной работы является реализациябинарного загрузчика, необходимого для корректного запуска гостевогомашинного кода, представленного в виде объектных файлов.
Остановимся наформате объектных файлов подробнее.В настоящее время в среди Linux (а значит и Android) единственнымиспользуемым форматом бинарного кода является ELF (Executable andLinkable Format — формат исполнимых и компонуемых файлов) [11]. Файлтакого формата состоит из нескольких частей (описания даны в виде19структур языка С, так как именно в таком виде с ними велась работа):1. Заголовок (ELF header). Содержит в себе необходимую информациюоб исполняемом файлеtypedef struct{unsigned charинформация */Elf32_HalfElf32_Half*/Elf32_WordElf32_Addr(стартовыйадресElf32_Offсегментов */Elf32_Off*/Elf32_Wordпроцессора */Elf32_HalfElf32_HalfElf32_HalfпрограммныхElf32_Halfсекций */Elf32_HalfзаголовковElf32_Halfтаблицy} Elf32_Ehdr;e_ident[EI_NIDENT];/* Сигнатура и прочаяe_type;/* Тип объектного файла */e_machine; /* Аппаратная платформа (архитектура)e_version; /* Номер версии */e_entry;/*АдресСмещениеточкивходапрограммы) */таблицы программныхe_phoff;/*e_shoff;/* Смещение таблицы заголовков секцийe_flags;/*Специфичныефлагиe_ehsize; /* Размер ELF-заголовка в байтах */e_phentsize; /* Размер записи в таблице программныхсегментов */e_phnum; /* Количество записей в таблицесегментов */e_shentsize; /* Размер записи в таблице заголовковe_shnum;/*Количество записей в таблицесекций */e_shstrndx; /* Расположение сегмента, содержащегостpок */2.
Таблица программных заголовков (сегментов). Каждая запись этойтаблицы содержит в себе информацию о соответствующем сегментебинарного файла – его содержании, типе, а также том, каким образом ондолжен быть загружен в память.typedef struct{20Elf32_Word p_type;Elf32_Off p_offset;Elf32_Addr p_vaddr;Elf32_Addr p_paddr;Elf32_Word p_filesz;Elf32_Word p_memsz;Elf32_Word p_flags;Elf32_Word p_align;} Elf32_Phdr;/* Тип программного сегмента *//* Смещение сегмента в файле *//* Виртуальный адрес сегмента для загрузки *//* Физический адрес сегмента для загрузки *//* Размер сегмента в файле *//* Размер сегмента в памяти при загрузке *//* Флаги и параметры сегмента *//* Выравнивание в памяти при загрузке */3. Таблица секций.
Каждый сегмент содержит в себе несколько секцийисполняемого файла. Каждая секция бинарного файла хранит определенныйвид данных.typedef struct{Elf32_Word sh_name; /* Имя секции */Elf32_Word sh_type;/* Тип секции */Elf32_Word sh_flags;/* Флаги и параметры */Elf32_Addr sh_addr;/* Адрес в памяти */Elf32_Off sh_offset; /* Смещение в файле */Elf32_Word sh_size;/* Размер в байтах */Elf32_Word sh_link;/* Смещение связанной секции */Elf32_Word sh_info;/* Информация */Elf32_Word sh_addralign;/* Выравнивание адреса */Elf32_Word sh_entsize; /* Размер одной записи */} Elf32_Phdr;Внастоящее время нет чёткой стандартизации именования исодержаниясекцийисполняемыхфайлов.Однаковотсамыераспространенные секции, встречающиеся в любой программе:.text – исполняемый код программы.
В архитектуре ARM данная секцияможет содержать исключительно инструкции процессора.data – инициализированные данные программы (константы, строки ит.д.).bss – не инициализированные данные, заполняются нулями при загрузке21.plt – Procedure Linkage Table. Содержит код стабов вызовов внешнихфункций, адреса которых лежат в .got, и могут быть не известны на этапекомпиляции (например если исполняемый файл использует динамическиебиблиотеки).rel – таблица релокаций.
список мест в секции .text, которые нужноизменить при связывании этого объектного файла с другими объектнымифайлами..got – Global Offset Table – глобальная таблица смещений. Содержитсмещения всех гобальных переменных, находящихся за пределами данногообъектного файла..symtab –таблица символов файла, содержит все внутренние ивснешние символы (глобальные переменные, функции)..strtab – таблица строк файла, содержащая имена символов, функций,отладочную информацию и т.д.Этот список далеко не полный, и всего лишь отражает наиболеераспространенныесекции,атакже секции, с которыми пришлосьнепосредственно столкнуться при написании загрузчика.Таким образом видно, что сам по себе формат исполняемых файлов ELFобладает сложной структурой, что влечёт за собой необходимость вопределенной внимательности к деталям.
Далее будет дано полное описаниеалгоритма загрузчика, который был реализован в ходе выполнения даннойработы.После инициализации vcpu, а также смежных с ним систем за делопринимаетсязагрузчик,получившийпутькисполняемомуфайлуархитектуры ARM. В первую очередь читает ELF header переданного файла,22и проверяет его на валидность, а также удостоверяется, что загрузчику былпередан файл, собраннуй под нужную архитектуру. Далее считываетсятаблица программных сегментов и подсчитывается количество страницпамяти, для размещения исполняемого файла в памяти. Так как это новыйпроцесс, то имеется возможность удовлетворить требованиям к размещениюпрограммных сегментов по тем виртуальным адресам, что были указаны вELF файле.Затем после выделения памяти под все сегменты, подлежащих загрузке впамять, начинается процесс их разбора. В первую очередь определяется типпрограммного сегмента.
В случае, если это PT_LOAD, то есть сегмент,предназначенный для загрузки, производится так называемый маппинг(отображение) части файла, который его содержит на соответствующую емуобласть виртуальной памяти. Отдельно в данном случае обрабатываетсясегмент данных, который как правило содержит секцию .bss – неинициализированные данные. Для экономии места в исполняемых файлахони хранятся лишь в виде обозначений размера, в виртуальной же памятипод эту секцию выделяется память, по умолчанию заполняемая нулями.Также при маппировании происходит учёт сдвигов виртуальных адресов,который происходит при выравнивании сегментов.
В данном случае этокритично, так как архитектура ARM требует, чтобы адреса машинныхинструкций в памяти были выровнены на 2 или 4 байта.В случае, когда типом сегмента является PT_DYNAMIC – это значит,что сегмент содержит информацию, необходимую для динамическогосвязывания объектных файлов (например при динамической загрузкебиблиотек).Информация представлена в виде таблицы ссылок на23соответствующие секции исполняемого файла.
Для дальнейшей работысохраняется динамическая таблица символов и таблица строк.После разбора всех программных сегментов начинается этап поиска иразрешения зависимостей. Для каждой записи в сегменте, отвечающем задинамическое связывание и имеющем тип DT_NEEDED сохраняем имянеобходимой внешней библиотеки. Далее для каждой из библиотек, откоторых зависит исходный исполняемый файл, необходимо рекурсивновызватьпроцедурулинковки.Такимобразомвпервуюочередьудовлетворяются самые глубокие зависимости, а при обратном ходерекурсии все необходимые библиотеки будут подгружены и слинкованы.На момент, когда в виртуальной памяти находятся все необходимыесегменты исходного исполняемого файла, а также библиотек, от которых онзависит, начинается процедура разрешения зависимостей.