User Tools

Site Tools


writingarmletswithgcc

Введение

Статья описывает специфику программирования армлетов с помощью 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)}
}
writingarmletswithgcc.txt · Last modified: 2005/10/14 13:27 by 127.0.0.1