Writing Armlets With Gcc

  • Введение
  • Теория
  • Gcc, компилирующий в ARM-код
  • Опции компилятора для простых армлетов
  • Позиционно-независимый код
  • Как избежать генерации GOT?
  • Вынос глобальных данных в структуру
  • Безопасное объявление константных строк
  • Как избежать указателей на функции
  • армлеты и C++
  • Ручное создание GOT
  • Опции, связанные с GOT
  • Загрузчик GOT
  • Известные ловушки .got
  • Глобальные данные
  • Введение

    Статья описывает специфику программирования армлетов с помощью GCC. Предполагается, что читатель знаком с пальмовой документацией по армлетам и функции PceNativeCall

    Теория

    Когда говорят о достижениях компьютерной мысли 5-60 годов, то, вспоминая про компиляторы и линкеры, часто забывают про еще один модуль: загрузчик. Загрузчик (loader) - это модуль, который преобразовывает программу на внешнем носителе в блок памяти с исполняемыми кодами, готовый к исполнению.

    В функции загрузчика входят:

    Я вспомнил про загрузчики по одной простой причине: компания Palm Source совершенно не описала в каком виде и как хранить армлет. Таким образом задачи загрузчика перекладываются на программиста.

    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:

    Позиционно-независимый код

    Очень быстро мы упираемся в то, что такой технологии недостаточно. Почему?

    Попытки использовать все это приводит к краху программы.

    В чем проблема?

    Как решают проблему с неизвестным адресом загрузки в других ОС? Обычно программа линкуется как начинающаяся с адреса 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

    использующая его устанавливает этот регистр.

    Вот какой год будет генерироваться при отсутствии -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). Для выделения лучше использовать комбинированное решение:

    Обратите внимание, что из-за необходимости склеивания мы потеряли преимущество Palm OS - исполнение кода без копирования.

    Глобальные данные

    Ниже приведен скрипт, позволяющий использовать инициализированные глобальные данные в армлетах. При этом генерируется 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)}
    }