User Tools

Site Tools


palmnativearmmodules

Введение

Настоящая статья описывает принципы построения нативных ARMовых модулей PalmOS 5.

Взгляд сверху

Для плавного перехода на современные процессоры, фирма Palm(Source) реализовала в 5 версии своей ОС два программных интерфейса: интерфейс, повторяющий архитектуру старых ОС на процессоре m68000, и новый, реализованный для процессоров семейства ARM. Новый интерфейс близок к старому, но некоторые детали изменились. Так, существенно изменилась работа с бинарными модулями, программами и библиотеками.

На смену старым приложениям, хранящим свой код в ресурсах code#1, code#0 и data#0, пришли новые, с ресурсами amdc#0, amdi#0 и amdd#0. В чем отличие?

Две части работы с модулями

PalmOS 5 разделила работу с модулями на две части. Первая часть находится в модуле DAL и носит название RAL, вторая содержится в ядре ОС (boot) и реализует собственно программный интерфейс. Зачем потребовалось такое разделение?

Дело в том, что PalmOS 5 существует в различных инкарнациях, как в виде железа ( на ARM процессоре), так и в виде симулятора ( на x86 с Win32 API слоем ниже). Все различия, связанные с нижележащими платформами вынесены в RAL, а системная часть исполняет общие вещи, в частности загрузку программы из базы.

Модули

Важно понимать, что RAL не оперирует с базами, он сразу работает с модулями в памяти. PalmOS работает в разделяемом адресном пространстве, так что все процесс, нити и прочая лабуда использует единый набор модулей.

Модуль идентифицируется идентификатором, числом от 0 до 0x3FF. Этот идентификатор выдается модулю при создании, что резко ограничивает возможность написания своих модулей.

Модуль может быть либо программой, либо библиотекой. Библиотека экспортирует некоторое количество функций. Функции экспортируются по номеру, причем возможна как явная загрузка модуля с получением адреса функции, так и неявная загрузка, позволяющая программисту просто обращаться к функции по имени из исходного кода. Неявная загрузка будет выполнена при первом вызове функции из модуля.

Каждый загруженный модуль состоит из двух блоков памяти, read-only блока кода и read-write блока данных.

Блок данных

Указатель на блок данных является определяющим для модуля, этот так называемый static base pointer. По адресу static base ( дальше SB ) можно найти полную информацию о модуле. Факт загруженности модуля определяется по ненулевому SB. SB указывает не на начало блока, а где-то ближе к началу, так что некоторые данные адресуются по отрицательным смещениям от SB.

Такой способ адресации был рекомендован компанией ARM как один из вариантов ABI. В регистре R9 хранится указатель на SB текущего модуля, все глобальные данные адресуются относительно SB. Также в любом SB[0] хранится указатель на массив всех SB, что позволяет быстро перегружать регистр при вызове функций другого модуля.

Блок данных состоит из следующих частей:

  • области системных данных
  • области ссылок на массив static base
  • области переходников на импортируемые модули
  • области изменяемых данных модуля

Область системных данных

Эта область содержит различные системные данные, позволяющие использовать модуль. Название PreStaticBase дано структуре из-за того, что она лежит по отрицательным смещениям от SB, который указывает после нее.

typedef struct {
	UInt32		data0Size;
	void *		data0;
	void *		code0;
	void *		code1;
	LocalID		dbH;
} RALCodeDescriptorType;
typedef struct PreStaticBase{
	RALCodeDescriptorType codeDesc;
	UInt32 dbUseCount;
	void * lastDispatchTablePtr;
	void **(entryTables[3]); //-C UI, -8 Boot, -4 DAL
}PreStaticBase;

Структура RALCodeDescriptorType содержит указатели на залоченные ресурсы базы программы и будет описана дальше. dbUseCount содержит счетчик ссылок на модуль. Каждый модуль, вызывающий функции из нашего увеличивает этот счетчик. lastDispatchTablePtr будет описана позже, в описании переходников на импортируемые модули. entryTables - это самый полезный элемент PreStaticBase. Этот элемент в каждом модуле указывает на одно и то же - на массив адресов системных вызовов трех модулей: DAL, Boot и UI. Всеми любимый код

LDR	R12, [[R9,|#-8]]
LDR	PC, [[R12,|#0x220]]

загружает из нужной таблицы указатель на системный вызов и переходит по нему. Это самый компактный способ вызова системного API. Еще раз замечу: в массивах лежат указатели на армовый код, исполняющий системные вызовы.

Область ссылок на массив SB

Непосредственно регистр SB указывает на 4 слова, содержащих указатели на массив SB для разных модулей. Получить конкретный SB можно по идентификатору модуля. Так, модуль ui с идентификатором 2 загружает свой SB так:

STR     R9, [SP,#-4]!	// сохранить старый SB
LDR     R9, [R9]	// загрузить указатель на массив всех SB
LDR     R9, [R9,#8]	// загрузить "наш" SB. 8 = 2 * sizeof(void *)

SB[0] указывает на начало массива.

SB[1] = SB[0] + 0x80, SB[2] = SB[0] + 0x100, SB[3] = SB[0] + 0x180.

В принципе три последних слова избыточны, но они позволяют генерировать короткий код загрузки SB для модулей с идентификаторами от 0x80 до 0x1FF.

Область переходников на импортируемые модули

typedef struct LinkDispatchEntry{
	void  **entries;
	UInt32 refNum;
}LinkDispatchEntry;

Дальше идет область переходников на импортируемые модули. Эта область используется при неявной загрузке модулей, используюшей функцию SysLinkerStub. Такая загрузка предполагает, что требуемый модуль будет загружен в ответ на вызов первой функнции, которую мы из него импортируем. Неявная загрузка удобна при написании программы, поскольку она не требует вызывать SysLoadModule и получать указатель на функцию. Неявность подразумевает, что загрузку модуля делает ОС, которая запоминает информацию о загруженном модуле в области переходников. Компилятор выделяет по одному переходнику на каждый используемый модуль, все вызовы функций одного модуля используют общий переходник. Перед использованием переходник обнулен. Первый вызов импортируемой функции видит 0 в поле entries, загружает требуемый модуль, и записывает его refNum и указатель на список экспортируемых функций в переходник. Последующие вызовы просто обращаются к функции через указатель в поле entries.

При загрузке библиотеки увеличивается счетчик ее использований dbUseCount. При выгрузке модуля ОС проходит по всем заполненным переходникам и уменьшает счетчик использования всех библиотек, указанных в refNum. Как найти где заканчивается массив переходников? Очень просто, при заполнении переходника модифицируется поле lastDispatchTablePtr в PreStaticBase так, что оно указывает на переходник с наибольшим адресом. Так что при выгрузке будут освобождены только те переходники, которые были инициализированы, то есть те, через которые происходили вызовы. Нюанс: если при инициализации переходника обнаружится перекрестные вызовы библиотек (например, на Treo650, HsNav вызывает функции из HsExtensions2.0 и наоборот), то у второго модуля поле entries будет заполнено, но поле refNum (указывающее на первый модуль) будет выставлено в ноль и счетчик ссылок на первый модуль не будет увеличено. Это позволит безопасно загружать библиотеки с перекрестными ссылками, и дает возможность правильной выгрузки библиотеки.

Второй нюанс: поскольку инициализация переходника изменяет lastDispatchTablePtr, то указание переходника в произвольном месте памяти (отличном от SB+0x16) может обрушить устройство при выгрузке, ибо цикл от SB+0x16 до lastDispatchTablePtr захватит добрую область памяти, содержащей совсем другие данные…

Область изменяемых данных модуля

За переходниками лежит область глобальных данных модуля. Инициализированные данные заполняются из базы. С помощью регистра SB программа адресуется к своим глобальным данным. Такая адресация не требует изменения ссылок на данные при загрузке модуля в память. Заметим, что именно сюда возвращается указатель функциями типа PalmOSGetGlobalsPtr.

Блок кода

Блок кода состоит из трех частей:

  • заголовок модуля
  • таблица экспорта
  • область кода и неизменяемых данных

Блок кода загружается из базы и не изменяется в процессе работы.

Заголовок модуля

Заголовок модуля описывает характеристики модуля.

typedef struct AmdcHeader{
	UInt8	jumpToPilotMain[4];
	UInt32	exportTableOffsetFromHeader;
	UInt16	moduleID;
	UInt16	moduleIDEx;
	UInt32	revision;
	UInt32	exportEntriesCount;
	char	type[4];
	char	creator[4];
	char	amdcType[4];
	UInt16	flags1;
	UInt16	flags2;
} AmdcHeader;

Первые 4 байта занимает переход на функцию PilotMain. Это не адрес, это инструкция, причем в ARMовой кодировке. PilotMain сохраняет традиции классического PalmOS, кроме вызова для показа UI, эта функция получает управление для инициализации и деинициализации библиотеки, а еще один полезный код позволяет обрабатывать вызов функций библиотеки из m68k через PACE.

Дальше идет смещение библиотеки экспортируемых функций от начала модуля. Обычно она располагается сразу после заголовка по смещению 0x24, но это необязательно. Вместе с полем exportEntriesCount, которое описывает число экспортируемых функций они полностью описывают второй компонент блока кода.

Дальше идут два поля, составляющих вместе идентификатор модуля. Причем

modRefID = moduleID/4 + (moduleIDEx/4) * 32

. У большинства модулей поле moduleIDEx обнулено, так что поле moduleID однозначно идентифицирует модуль. Так, у модуля CPM Library moduleID содержит 132, так что идентификатор у модуля 33. А вот у модуля SSL Crypto Library moduleID=0, а moduleIDEx=4, так что у модуля идентификатор 32. Зачем так сделано? Неизвестно. Важно понять, что результирующий идентификатор должен быть уникальным, а как он вычисляется из двух полей - неважно.

Поле revision хранит ревизию модуля. На всякий случай.

Поля type, creator и amdcType в принципе описывают соответствующие поля базы, откуда программа загрузилась. Если бы. Модуль MImgPlugIn.prc с Treo 650 в базе имеет тип CdPl, создателя ImCp, а в модуле создателем почему-то указан Mjpg. Модуль boot.prc, вроде бы солидный модуль, а туда же: у базы тип rsrc, создатель psys, а у модуля тип boot. Используется ли это поле в заголовке при поиске библиотеки по типу, или нет - неизвестно. amdcType по идее хранит тип ресурса в базе, откуда загружается программа. Тоже нет, у boot.prc исполняемый код хранится в ресурсе boot#0x2713, а в поле amdcType хранится 'amdc'. Все поля приведены в little endian нотации, так что при просмотре ресурсов вы увидите все поля с перевернутыми байтами. Вообщем непонятные поля, скорее всего в настоящее время они не используются.

В последних полях хранятся флаги, указывающие на специфические свойства модулей. Обычно первый флаг содержит 2, а второй 4. Для системных модулей во втором флаге хранится 7.

#define RAL_AMDC_FLAG1_VALID_MODULE 2
#define RAL_AMDC_FLAG2_VALID_MODULE 4
#define RAL_AMDC_FLAG2_SYSTEM_MODULE 3

RAL_AMDC_FLAG1_VALID_MODULE и RAL_AMDC_FLAG2_VALID_MODULE выставляется всем, а RAL_AMDC_FLAG2_SYSTEM_MODULE может стоять только у dal, boot, ui.

Таблица экспорта

Местоположение и размер таблицы экспорта указывается в полях exportTableOffsetFromHeader и exportEntriesCount. Таблица экспорта - это массив 4-байтовых инструкций перехода на экспортируемые функции. Не указатели, а сам армовый код перехода на функции.

Область кода

Сало - они и есть сало. Сюда компилятор кидает код функций, здесь осуществляются все переходы из arm в thumb и обратно. Здесь же лежат константные данные. По понятным причинам генерируемый код должен быть PIC ( position independent code), что для армового кода проблем не вызывает.

RAL

Как сказано выше, модуль RAL представляет самый нижний уровень работы с модулями. Он привязан к платформе и выполняет платформозависимый код. RAL API описывает абстрактный модуль и операции с ним.

Что входит в RAL API? Все, что было описано выше про внутренности модулей скрыто в RAL API.

typedef struct {
	UInt32		data0Size;	/* 'amdd' resource size */
	MemPtr		data0;		/* 'amdd' resource pointer */
	MemPtr		code0;		/* 'amdi' resource pointer */
	MemPtr		code1;		/* 'amdc' resource pointer */
	MemHandle	dbH;		/* program database memhandle */
} RALCodeDescriptorType;

typedef struct {
	UInt32		refNum;
	UInt32		dataSize;
	UInt32		revision;
	UInt32		entries; // entries count
	UInt8		sharingAppID;
} RALModuleInfoType;

Err RALCallWithNewStack(void *procP, void *procArg, void *newStack);
Err RALGetStaticBase(UInt32 refNum, void **staticBaseP);
Err RALGetEntryAddresses(UInt32 refNum, UInt32 startEntryNum, UInt32 endEntryNum, void **addressesPP);
Err RALGetModuleInfo(RALCodeDescriptorType *codeDescriptorP, RALModuleInfoType *moduleInfoP);
Err RALLinkClient(UInt32 refNum, UInt32 clientID, void **dispatchTablePP);
Err RALLoadModule(RALCodeDescriptorType *codeDescriptorP, void *dataSegmentP, PalmOSMainFuncType **mainEntryPP, UInt32 *refNumP);
Err RALPatchEntry(UInt32 refNum, UInt32 entryNum, void *procP, void **oldProcPP);
Err RALSetA5(UInt32 newValue);
Err RALUnloadModule(UInt32 refNum, RALCodeDescriptorType *codeDescriptorP, void **dataSegmentPP);
Err RALUnloadNext(UInt32 refNum, RALCodeDescriptorType *codeDescriptorP, void **dataSegmentPP);

Функция RALCallWithNewStack позволяет вызывать функцию на новом стеке. Это действие абстрагирует для SysAppLaunch запуск нового подпроцесса с новым стеком.

Функция RALGetStaticBase возвращает указатель на SB.

Функция RALGetEntryAddresses возвращает для загруженного модуля либо указатель на точку входа, если startEntryNum равно -1, либо копирует в переданный массив адреса экспортируемых функций в диапазоне startEntryNum..endEntryNum.

Функция RALGetModuleInfo возвращает информацию, прочитанную из RALCodeDescriptorType в структуре RALModuleInfoType.

Функция RALLinkClient инициализирует переходник вызова из библиотеки с идентификатором clientID библиотеку с идентификатором refNum.

Функция RALLoadModule пользуясь структурой RALCodeDescriptorType, указывающей на ресурсы из базы программы, осуществляет загрузку модуля. В качестве блоков данных и кода используются параметры codeDescriptorP и dataSegmentP. Функция возвращает указатель на точку входа и идентификатор модуля.

Функция RALUnloadModule выгружает модуль, заполняет структуру RALCodeDescriptorType и возвращает указатель на блок данных для освобождения ресурсов.

Функция RALPatchEntry патчит обработчик функции в модуле. Работает только для модулей dal, boot, ui.

RALSetA5, RALUnloadNext - ?

Структура базы программы

Армовая программа имеет тот же тип, что и классическая программа - 'appl'. Разные типы программ различает функция SysAppLaunch, которая проверяет типы ресурсов. Аромвая программа обычно содержит три ресурса: amdc#0, amdi#0 и amdd#0.

amdi#0 - это аналог code#0, правда из всех данных оттуда используется слово по смещению 4. Там лежит размер области данных выше SB (по умолчанию 0x10 - размер области указателей на SB) . Размер области ниже SB фиксирован и равен размеру структуры PreStaticBase ( 0x28 байт).

amdd#0 - традиционно компрессированный образ блока данных. Он разжимается в область после SB.

amdc#0 - это ресурс с кодом данных. Он начинается с заголовка описанного выше. Обычно этот ресурс не копируется в память, то есть он исполняется из storage или dbcache.

В армовой модели программ не предусмотрено никаких fixupов кода. Размер ресурса кода неограничен. Кстати из-за этого может вылезти проблема общения нативного приложения с hotsync, которые не переваривает ресурсов > 64К.

Самое забавное, что типы и индексы ресурсов не фиксированы. У функции SysLoadModule можно указать тип ресурса, отличный от amdc. При загрузке библиотеки через SysLinkerStub вообще можно задать типы и индексы всез трех ресурсов в структуре SysModuleDescriptorType. Так, системный модуль boot.prc почему-то предпочитает тип boot.

Функции из ядра PalmOS

Что же делают функции высокого уровня? Они оборачивают низкоуровневые функции, занимаясь грязной работой типа выделения памяти, поиска и открытия баз.

typedef struct {
	UInt32		dbType;
	UInt32		dbCreator;
	UInt32		revision;
	UInt32		entries;
	UInt32		rsrcTypeData0;
	UInt32		rsrcTypeCode0;
	UInt32		rsrcTypeCode1;
	UInt16		rsrcIDData0;
	UInt16		rsrcIDCode0;
	UInt16		rsrcIDCode1;
	UInt16		reserved;
} SysModuleDescriptorType;


Err SysFindModule(UInt32 dbType, UInt32 dbCreator, UInt16 rsrcID, UInt32 flags, UInt32 *refNumP);
Err SysPatchEntry(UInt32 refNum, UInt32 entryNum, void *procP, void **oldProcP);
Err SysGetEntryAddresses(UInt32 refNum, UInt32 entryNumStart, UInt32 entryNumEnd, void **addressP);
Err SysLoadModule(UInt32 dbType, UInt32 dbCreator, UInt16 rsrcID, UInt32 flags, UInt32 *refNumP);
Err SysUnloadModule(UInt32 refNum);
void SysLinkerStub(SysModuleDescriptorType *moduleP, UInt32 clientID, void **dispatchTablePP);

Функции SysFindModule и SysLoadModule загружают модули. Отличие между функциями неизвестно. Функции открывают базы, лочат кодовые ресурсы, подсчитывают размер, выделяют область данных и вызывают RALLoadModule

Функция SysUnloadModule выгружает модуль. RALUnloadModule отвечает за низкий уровень, а SysUnloadModule чистит память и закрывает базу.

Функции SysPatchEntry и SysGetEntryAddresses - это аналоги соответствующих функций из RAL. Интересно, что у Zodiac вызов SysPatchEntry был запрещен, в отличие от RALPatchEntry.

Функция SysLinkerStub ( по ссылке можно найти документированный код, использующий эту функцию) вызывает RALLoadModule по информации из SysModuleDescriptorType и далее вызывает RALLinkClient.

P.S. Прототипы функций частично взяты из mobilestream sdk.

palmnativearmmodules.txt · Last modified: 2007/02/27 21:36 by 127.0.0.1