Table of Contents
Введение
Статья описывает специфику программирования армлетов с помощью GCC. Предполагается, что читатель знаком с пальмовой документацией по армлетам и функции PceNativeCall
Теория
Когда говорят о достижениях компьютерной мысли 5-60 годов, то, вспоминая про компиляторы и линкеры, часто забывают про еще один модуль: загрузчик. Загрузчик (loader) - это модуль, который преобразовывает программу на внешнем носителе в блок памяти с исполняемыми кодами, готовый к исполнению.
В функции загрузчика входят:
- Выделение памяти для программного кода и копирование его с внешнего носителя
- Выделение памяти для инициализированных данных и инициализация (int x = 3;)
- Выделение памяти для неинициализированных данных (int y);
- Выделение памяти под стек.
- Коррекция адресов памяти
Я вспомнил про загрузчики по одной простой причине: компания PalmSource совершенно не описала в каком виде и как хранить армлет. Таким образом задачи загрузчика перекладываются на программиста.
Gcc, компилирующий в ARM-код
Использовать Gcc для компиляции армлетов выглядит вполне логично, ведь к анонсированию технологии армлетов уже существовал кодогенератор Gcc в ARM-код. Попробуем понять, насколько он пригоден.
Gcc - компилятор, который достаточно сильно привязан к юниксоподобным ОС. И любимый файл, получающийся после сборки линкером - это файл формата ELF. Такие файлы содержат внутри себя именованные сегменты (секции) с кодом и данными. Секция с кодом традиционно называется .text . Сразу возникает мысль - скомпилировать программу в ELF и извлечь оттуда эту секцию. Секцию поместить в ресурс и в нужное время залочить ресурс и передать на него управление.
Пример вызова:
UInt32 CallARM(UInt32 id, void *param){ MemHandle h; UInt32 res; void *p; h = DmGet1Resource('armc', id); p = MemHandleLock(h); res = PceNativeCall(p, param); MemHandleUnlock(h); DmReleaseResource(h); return res; }
Для самых простых случаев такой метод годится. Секцию можно извлечть с помощью утилиты obj-copy.
Опции компилятора для простых армлетов
Для самых простых армлетов достаточно трех специфических опции gcc:
- -fPIC - генерировать позиционно-независимый код (position independent code). Эту опцию нужно включать для того, чтобы код мог работать с любого адреса.
- -ffixed-r9 - PalmOS использует регистр R9 для системных данных. Опция запрещает компилятору использовать регистр.
- -nostartfiles - Опция указывает линкеру, что startup-библиотека не нужна.
Позиционно-независимый код
Очень быстро мы упираемся в то, что такой технологии недостаточно. Почему?
- Нельзя использовать глобальные данные.
- Не работает получение адресов функций (
void (*pfn)(int) = foo;
).
- не работают константные строки (
strcpy(sz, "test");
).
Попытки использовать все это приводит к краху программы.
В чем проблема?
- Во-первых мы не берем глобальные данные из секции .data
- Во-вторых даже если мы их возьмем, то мы не знаем как их подсунуть бинарному коду
- В-третьих компилятор и линкер в принципе не может знать с какого адреса будет загружена наша программа и данные. А без знания этого он не может вычислить адрес функции или строки.
Как решают проблему с неизвестным адресом загрузки в других ОС? Обычно программа линкуется как начинающаяся с адреса 0, а загрузчик прибавляет реальный стартовый адрес ко всем ссылкам, которые требуют настоящего адреса. Такие ссылки называются relocation или fixup, а создаваемый список носит имя relocation list.
Поскольку у нас юниксовый GCC, то перемещаемый код ассоциируется прежде всего с динамическими библиотеками (shared libraries). И технология используется соответствующая - GOT (global offset table). Технология использования GOT такая: все секции программы рассматриваются как один монолитный блок памяти, загрузчик проходит по секции .got и прибавляет ко всем 4-байтовым словам стартовый адрес. Все фрагменты кода, которые требуют абсолютного адреса просто считывают его из GOT.
Пример:
==== void foo(void); void bar(void){ void *p; p = &foo; } void foo(void){ } ==== bar: LDR R3, =0 @ загрузить в R3 индекс ссылки на foo LDR R3, [R10,R3] @ загрузить в R3 адрес функции. R10 - указатель на GOT STR R3, [R11,#-0x10]@ запомнить адрес в локальной переменной. R11 - указатель на фрейм локальных переменных .got: DCD foo-base_address @ слово из GOT со смещением foo
Как избежать генерации GOT?
Ответ простой: не использовать то, что генерирует записи для GOT. То есть не использовать глобальных данных, указателей на функции, константных строк итд. Весело, да?
Вынос глобальных данных в структуру
Если без глобальных данных не получается, то можно поступить следующим образом: собрать все глобальные данные и вынести их в структуру. Все ссылки на псевдо-глобальные данные пойдут через ссылку. Для скорости можно указать, что указатель всегда содержится в регистре. Стоит запретить использование регистра компилятором с помощью опции -ffixed-r8.
Например,
// структура с глобальными данными struct MyGlobals{ int x; // ... }; // Глобальная переменная!!! Компилятор вместо нее будет использовать регистр R8. register struct MyGlobals *global asm ("r8"); // инициализация R8 в начале кода // предполагается, что блок глобалных данных выделяет основной код и передает параметром. unsigned long NativeFunc (const void *emulStateP, char *userData68KP, Call68KFuncType *call68KFuncP) { asm("ldr r8,%0" : "=m" (userData68KP)); //... // пример использования // int z = x + 3; int z = global->x + 3; }
Безопасное объявление константных строк
Я разработал макрос CONST_TEXT, который объявляет мини-функцию, возвращающую указатель на строку. В отличии от простого использования константной строки, мой метод позволяет обойтись без GOT.
// определение макроса #define CONST_TEXT(name, str) \ static const char * const name(void){ \ asm("ADR R0, 1f; B 2f; 1: .ASCIZ \"" str "\"; .ALIGN 4; 2: ;"); \ } // объявление переменной CONST_TEXT(getNote, "Enter custom note: "); // использование const char *const prompt = getNote();
Как избежать указателей на функции
Единственный хороший метод избежать GOT - вынести функции в отдельные ресурсы и передавать указатели на них.
армлеты и C++
Рецепт простой: не используйте C++, если вы не понимаете как реализованы конструкции этого языка. Если вы не можете навскидку сказать где хранятся VMT, то забудьте об использовании плюсов в армлетах.
Ручное создание GOT
Если вы не можете использовать не GOT, научитесь ее обрабатывать. Это несложно. Поместите секцию .got в отдельный ресурс, склейте .text и .got при исполнении и прибавьте адрес блока ко всем записям в GOT. Патченый build-prc можно взять в YAHM SDK по адресу http://yahm.palmoid.com/yahmv.htm . Патч позволяет автоматически заносить .got секцию в получаемый prc-файл
Опции, связанные с GOT
- -mpic-register=r10 - опция указывает на регистр, в котором хранится указатель на GOT. Обратите внимание, что разные армлеты могут иметь разные GOT.
- -msingle-pic-base - опция говорит, что установкой указателя на GOT занимается startup. Если опция не указана, то каждая функция,
использующая его устанавливает этот регистр.
Вот какой год будет генерироваться при отсутствии -msingle-pic-base:
LDR R10, =_got_start - . - 8@ загрузить в R10 разницу между адресом GOT и текущим адресом. ADD R10, PC, R10 @ прибавить значение текущего адреса
Код, который нужно исполнить при старте очевиден:
asm("ldr r10,%0" : "=m" (gotPtr));
.
Загрузчик GOT
Приведу пример загрузчика GOT из http://yahm.palmoid.com. Функция принимает указатель на код и номер ресурса с GOT. На выходе мы получаем указатель на новый откорректированный блок памяти или на старый код, если GOT отсутствует.
inline UInt32 RoundCeil4(UInt32 x){ return ((x + 3) / 4) * 4; } void *FixupCode(void *codeInResource, UInt16 resNo, UInt32 *pGotPtr){ MemHandle hGot; UInt32 *pFixups; void *pChunk; UInt32 codeSize, gotSize; int i; *pGotPtr = 0; hGot = DmGet1Resource('.got', resNo); if (hGot ===== NULL){ // no fixup section return codeInResource; } codeSize = MemPtrSize(codeInResource); gotSize = MemHandleSize(hGot); pChunk = MemPtrNew(RoundCeil4(codeSize) + gotSize); if (pChunk ===== NULL) return NULL; pFixups = (UInt32 *)(pChunk + RoundCeil4(codeSize)); *pGotPtr = (UInt32)pFixups; MemMove(pChunk, codeInResource, codeSize); MemMove(pFixups, MemHandleLock(hGot), gotSize); MemHandleUnlock(hGot); DmReleaseResource(hGot); for(i = 0; i < gotSize/sizeof(UInt32); ++i){ UInt32 x = ByteSwap32(pFixups[i]) + (UInt32)pChunk; pFixups[i] = ByteSwap32(x); } return pChunk; }
Известные ловушки .got
Ловушка #1: компилятор полагает, что .got идет в порядке, указанном в скрипте линкера (то есть после .text). Наличие дополнительных секций типа .disposn может сдвинуть GOT. В этом случае следует использовать свой скрипт для линкера. Скрипт задается параметром -Xlinker -T -Xlinker myscript.ls.
/* Script for -z combreloc: combine and sort reloc sections */ OUTPUT_FORMAT ("elf32-littlearm", "elf32-bigarm", "elf32-littlearm") OUTPUT_ARCH ("arm") SEARCH_DIR("/usr/arm-palmos/lib"); /* This is a pathetically simple linker script that is only of use for building Palm OS 5 armlets, namely stand-alone code that has no global data or other complications. */ SECTIONS { .text : { *(.text .rodata) *(.glue_7t) *(.glue_7) } .got : {*(.got) } .got.plt : {*(.got.plt) } .disposn : { *(.disposn) } .data : {*(.data)} .bss : {*(.bss)} }
Обратите внимание, что даже при наличии опции -msingle-pic-base GCC может адресоваться к GOT не по регистру, а через смещение от начала кода ( например, http://www.escribe.com/computing/poaf/m911.html ). Это разбивает в прах светлую идею выделять память только для GOT, с адресацией через R10.
Проблема #2: что делать, если объем секции .text больше 64K. Понятно, что большой кусок нужно распилить на несколько ресурсов и в процессе исполнения склеить. Для выделения большого блока можно использовать функцию MemGluePtrNew или выделить через FtrPtrNew. Первая функция выделяет в динамической памяти, а вторая в памяти баз (и записывать в нее нужно через DmWrite). Для выделения лучше использовать комбинированное решение:
- Для TT с 1Мб кучи лучше записывать с помощью FtrPtrNew
- Для NX-60 с 3Мб кучи запись с помощью MemGluePtrNew логичнее
Обратите внимание, что из-за необходимости склеивания мы потеряли преимущество PalmOS - исполнение кода без копирования.
Глобальные данные
Ниже приведен скрипт, позволяющий использовать инициализированные глобальные данные в армлетах. При этом генерируется GOT и получаемый образ должен быть в куче.
Используйте опции -Xlinker -T -Xlinker globalscript.ls для указания скрипта.
/* Script for -z combreloc: combine and sort reloc sections */ OUTPUT_FORMAT ("elf32-littlearm", "elf32-bigarm", "elf32-littlearm") OUTPUT_ARCH ("arm") SEARCH_DIR("/usr/arm-palmos/lib"); /* This is a pathetically simple linker script that is only of use for building Palm OS 5 armlets, namely stand-alone code that has no global data or other complications. */ SECTIONS { .text : { *(.text .rodata) *(.glue_7t) *(.glue_7) *(.data) } .got : {*(.got) } .got.plt : {*(.got.plt) } .disposn : { *(.disposn) } .data : {LONG(1)} .bss : {*(.bss)} }