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