10 особенностей архитектуры ARM (ARMv4T)

Ниже представлены 10 особенностей ARM-архитектуры, которые, по-моему мнению, являются интересными и заслуживающими внимания. Люди, занимающиеся программированием под ARM (в особенности с использованием ассемблера) врядли найдут здесь что-то новое, однако новички и люди, знакомые с прочими архитектурами (AVR, MIPS, x86 и прочими) могут найти интересные вещи.

Все написанное применимо для ARMv4T (ARM7TDMI-S), однако, некоторые особенности встречаются и в ARMv7 (Cortex-Mx).

  1. Любая инструкция допускает условное выполнение. Специально для этого 4 бита отведены под флаги условного выполнения. Это хорошо видно в таблице, представленной в официальной документации:

    Это позволяет сооружать такие хитрые конструкции:

    ; if (r0 == 0)
    ;   r0 = 2;
    ; else
    ;   r0 = 1;
    cmp   r0, #0 
    moveq r0, #2 
    movne r0, #1 
    
  2. Отсутствует возможность кодирования в инструкции константы с числом значащих бит более 8. Именно по этой причине, чтобы занести какую-то 32-битную константу в регистр компилятор вынужден создавать следующие конструкции:

    ------------------------------------------------------
    ; Данная конструкция замечена за GCC:
    mov	r0, #0x12, lsl #24
    or	r0, r0, #0x34, lsl #16
    or	r0, r0, #0x56, lsl #8
    or	r0, r0, #0x78
    ------------------------------------------------------
    ; Но более распространен вариант:
    ...
    ldr r0, [pc+#const1_offset]
    ...
    const1:	DD 0x12345678
    ------------------------------------------------------

    При втором варианте, компилятору приходится создавать в конце функций блоки с данными, которые используются в данных функциях, а в случае превышения некоторого объема и вовсе разрывать функции, чтобы поместить данные внутри, а инструкцией LDR rX, [pc+#xx] получилось дотянуться.

  3. Отсутствие инструкций PUSH/POP в привычном виде. Для работы со стеком используются инструкции LDM, STM. При этом они могут оперировать с любым регистром. Т.е. указателем стека может служить любой регистр. Однако, в целях совместимости принято, что R13 используется в качестве стекового регистра, он же имеет синоним SP.

    ------------------------------------------------------
    ; Сохраняем регистры, требуемые в функции. Так же сохраняем адрес 
    ; возврата из функции, это позволяет осуществлять вложенные вызовы.
    stm sp, {R0,R1, R3-R8, lr}
    ; Восстановить используемые регистры при выходе из функции, 
    ; при этом LR восстанавливается в PC, порождая тем самым 
    ; возврат из функции.
    ldm sp, {R0,R1, R3-R8, pc}
    ------------------------------------------------------
  4. Адрес возврата требуется заносить в стек вручную. Это видно на примере из пункта 3. Инструкции, используемые для вызова функций только заносят адрес возврата в регистр R14, который так же имеет синоним LR. В отдельных случаях потребуется формировать адрес возврата вручную (см. пример из пункта 9).

  5. Особенности чтения не выровненных данных. Для корректного чтения может потребоваться пользоваться специфичными макросами компилятора или читать данные по частям.

    ------------------------------------------------------
    unsigned char data[5] = { 0x11, 0x22, 0x33, 0x44, 0x55 };
    unsigned long tmp = *(unsigned long*)&data[1];
    printf("tmp = 0x%08x;rn", tmp);
    ------------------------------------------------------
    tmp = 0x11443322; /* Вместо ожидаемого 0x55443322 */
    ------------------------------------------------------

    Для получения корректного результата в Keil Realview MDK достаточно добавить макрос __packed:

    ------------------------------------------------------
    unsigned char data[5] = { 0x11, 0x22, 0x33, 0x44, 0x55 };
    unsigned long tmp = *(__packed unsigned long*)&data[1];
    printf("tmp = 0x%08x;rn", tmp);
    ------------------------------------------------------
    tmp = 0x55443322;
    ------------------------------------------------------
  6. Для каждого режима работы процессора выделен свой стек. Мало того, во всех режимах банк регистров частично заменяется (а в режиме FIQ и вовсе заменяется половина всех регистров). В таблице ниже представлены регистры для каждого режима и помечены заменяемые регистры.

    Сделано это для ускорения входа-выхода из прерываний, благодаря этому требуется сохраненять и восстанавливать меньше данных.

  7. Для вызова функций, возврата из функций и осуществления прыжков используется один и тот же класс инструкций.

    ------------------------------------------------------
    ...
    ; Вызов 1
    bl		func1
    ; Вызов 2 
    ; Обычно используется по вызову функции по указателю или 
    ; просто очень дальнему вызову, когда BL "не дотягивается".
    ldr		r0, =func1
    mov		lr, pc
    bx		r0
    ; Вызов 3 (возможен на ARM9):
    ldr		r0, =func1
    blx		r0
    ...
    
    func1:
    ; Тело функции
    bx		lr
    ------------------------------------------------------
  8. Первые 4 параметра функции передаются через регистры. Собственно ничего особо удивительного в этом нет. Мало того, при программировании на ассемблере это очень удобно.

    ------------------------------------------------------
    func(0x01, 0x02);
    ------------------------------------------------------
    mov		r0, #0x01
    mov		r1, #0x02
    bl		func
    ------------------------------------------------------
  9. Наличие 2х различных наборов команд: ARM и Thumb. Набор Thumb - это упрощенный и ужатый до 16 бит набор инструкций ARM. Инструкции Thumb при помощи специального подключаемого дешифратора декодируются до аналогичных ARM-инструкций (само ядро всегда обрабатывает ARM-код, перекодировка Thumb возложена на отдельный блок). По факту Thumb - подмножество команд ARM. Двойной набор команд - мера вынужденная по причине низкой плотности (т.е. большого объема) ARM-кода. Аналогичные меры предпринимались в архитектуре MIPS. На сколько мне известно, там так же был введен дополнительный набор команд с половинной длиной.

    Фиксированная длина инструкций. Все инструкции режима ARM имеют фиксированную длину 32 бита. Инструкции режима Thumb в большинстве своем имеют длину 16 бит, за исключением инструкций дальних прыжков, которые имеют длину 32 бита. Однако, фиксированная длина инструкций - это привычное дело для многих RISC-архитектур. Тот же MIPS32 имеет фиксированную длину инструкций.

  10. Единое адресное пространство кода и данных. А главное, возможность исполнения кода из ОЗУ и ПЗУ. Для "взрослых" арзитектур типа x86, PowerPC, MIPS и прочих - это привычное дело, однако, в микроконтроллерах PIC, AVR, MSC-51 данной возможности нет, мало того, там присутствуют определенные проблемы при использовании констант в ПЗУ.

Original: http://igorkov.org/arm10,
Author: igorkov