==== Введение ==== Статья описывает специфику программирования армлетов с помощью 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 из [[YAHMа|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)} }