Статья описывает специфику программирования армлетов с помощью GCC. Предполагается, что читатель знаком с пальмовой документацией по армлетам и функции PceNativeCall
Когда говорят о достижениях компьютерной мысли 5-60 годов, то, вспоминая про компиляторы и линкеры, часто забывают про еще один модуль: загрузчик. Загрузчик (loader) - это модуль, который преобразовывает программу на внешнем носителе в блок памяти с исполняемыми кодами, готовый к исполнению.
В функции загрузчика входят:
Я вспомнил про загрузчики по одной простой причине: компания PalmSource совершенно не описала в каком виде и как хранить армлет. Таким образом задачи загрузчика перекладываются на программиста.
Использовать 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:
Очень быстро мы упираемся в то, что такой технологии недостаточно. Почему?
void (*pfn)(int) = foo;
).
strcpy(sz, "test");
).
Попытки использовать все это приводит к краху программы.
В чем проблема?
Как решают проблему с неизвестным адресом загрузки в других ОС? Обычно программа линкуется как начинающаяся с адреса 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. То есть не использовать глобальных данных, указателей на функции, константных строк итд. Весело, да?
Если без глобальных данных не получается, то можно поступить следующим образом: собрать все глобальные данные и вынести их в структуру. Все ссылки на псевдо-глобальные данные пойдут через ссылку. Для скорости можно указать, что указатель всегда содержится в регистре. Стоит запретить использование регистра компилятором с помощью опции -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++, если вы не понимаете как реализованы конструкции этого языка. Если вы не можете навскидку сказать где хранятся VMT, то забудьте об использовании плюсов в армлетах.
Если вы не можете использовать не GOT, научитесь ее обрабатывать. Это несложно. Поместите секцию .got в отдельный ресурс, склейте .text и .got при исполнении и прибавьте адрес блока ко всем записям в GOT. Патченый build-prc можно взять в YAHM SDK по адресу http://yahm.palmoid.com/yahmv.htm . Патч позволяет автоматически заносить .got секцию в получаемый prc-файл
использующая его устанавливает этот регистр.
Вот какой год будет генерироваться при отсутствии -msingle-pic-base:
LDR R10, =_got_start - . - 8@ загрузить в R10 разницу между адресом GOT и текущим адресом. ADD R10, PC, R10 @ прибавить значение текущего адреса
Код, который нужно исполнить при старте очевиден:
asm("ldr r10,%0" : "=m" (gotPtr));
.
Приведу пример загрузчика 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; }
Ловушка #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). Для выделения лучше использовать комбинированное решение:
Обратите внимание, что из-за необходимости склеивания мы потеряли преимущество 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)} }