Школа ассемблера: язык ассемблера для центральных процессоров архитектуры ARM. Школа ассемблера: язык ассемблера для центральных процессоров архитектуры ARM Основные правила записи программ на ассемблере

Привет всем!
По роду деятельности я программист на Java. Последние месяцы работы заставили меня познакомиться с разработкой под Android NDK и соответственно написание нативных приложений на С. Тут я столкнулся с проблемой оптимизации Linux библиотек. Многие оказались абсолютно не оптимизированы под ARM и сильно нагружали процессор. Ранее я практически не программировал на ассемблере, поэтому сначала было сложно начать изучать этот язык, но все же я решил попробовать. Эта статья написана, так сказать, от новичка для новичков. Я постараюсь описать те основы, которые уже изучил, надеюсь кого-то это заинтересует. Кроме того, буду рад конструктивной критике со стороны профессионалов.

Введение
Итак, для начала разберёмся что же такое ARM. Википедия дает такое определение:

Архитектура ARM (Advanced RISC Machine, Acorn RISC Machine, усовершенствованная RISC-машина) - семейство лицензируемых 32-битных и 64-битных микропроцессорных ядер разработки компании ARM Limited. Компания занимается исключительно разработкой ядер и инструментов для них (компиляторы, средства отладки и т. п.), зарабатывая на лицензировании архитектуры сторонним производителям.

Если кто не знает, сейчас большая часть мобильных устройств, планшетов разработаны именно на этой архитектуре процессоров. Основным преимуществом данного семейства является низкое энергопотребление, благодаря чему он часто используется в различных встроенных системах. Архитектура развивалась с течением времени, и начиная с ARMv7 были определены 3 профиля: ‘A’(application) - приложения, ‘R’(real time) - в реальном времени,’M’(microcontroller) - микроконтроллер. Историю разработки этой технологии и другие интересный данные вы можете прочитать в Википедии или погуглив в интернете. ARM поддерживает разные режимы работы (Thumb и ARM, кроме того в последние время появился Thumb-2, являющийся смесью ARM и Thumb). В данной статье рассмотрим собственно режим ARM, в котором исполняется 32-битный набор команд.

Каждый ARM процессор создан из следующих блоков:

  • 37 регистров (из которых видимых при разработке только 17)
  • Арифметико-логи́ческое устройство (АЛУ) - выполняет арифметические и логические задачи
  • Barrel shifter - устройство, созданное для перемещения блоков данных на определенное количество бит
  • The CP15 - специальная система, контроллирующая ARM сопроцессоры
  • Декодер инструкций - занимается преобразованием инструкции в последовательность микроопераций
Это не все составляющие ARM, но углубление в дебри построения процессоров не входит в тему данной статьи.
Конвейерное исполнение (Pipeline execution)
В ARM процессорах используется 3-стадийный конвейер (начиная с ARM8 был реализова 5-стадийный конвейер). Рассмотрим простой конвейер на примере процессора ARM7TDMI. Исполнение каждой инструкции состоит из трёх ступеней:

1. Этап выборки (F)
На этом этапе инструкции поступают из ОЗУ в конвейер процессора.
2. Этап декодирования (D)
Инструкции декодируются и распознаётся их тип.
3. Этап исполнения (E)
Данные поступают в ALU и исполняются и полученное значение записывается в заданный регистр.

Но при разработке надо учитывать, что, есть инструкции, которые используют несколько циклов исполнения, например, load(LDR) или store. В таком случае этап исполнения (E) разделяется на этапы (E1, E2, E3...).

Условное выполнение
Одна из важнейших функций ARM ассемблера - условное выполнение. Каждая инструкция может исполняться условно и для этого используются суффиксы. Если суффикс добавляется к названию инструкции, то прежде чем выполнить ее, происходит проверка параметров. Если параметры не соответствуют условию, то инструкция не выполняется. Суффиксы:
MI - отрицательное число
PL - положительное или ноль
AL - выполнять инструкцию всегда
Суффиксов условного выполнения намного больше. Остальные суффиксы и примеры прочитать в официальной документации: ARM документация
А теперь пришло время рассмотреть…
Основы синтаксиса ARM ассемблера
Тем, кто раньше работал с ассемблером этот пункт можно фактически пропустить. Для всех остальных опишу основы работы с этим языком. Итак, каждая программа на ассемблере состоит из инструкций. Инструкция создаётся таким образом:
{метка} {инструкция|операнды} {@ комментарий}
Метка - необязательный параметр. Инструкция - непосредственно мнемоника инструкции процессору. Основные инструкции и их использование будет разобрано далее. Операнды - константы, адреса регистров, адреса в оперативной памяти. Комментарий - необязательный параметр, который не влияет на исполнение программы.
Имена регистров
Разрешены следующие имена регистров:
1.r0-r15

3.v1-v8 (переменные регистры, с r4 по r11)

4.sb and SB (статический регистр, r9)

5.sl and SL (r10)

6.fp and FP (r11)

7.ip and IP (r12)

8.sp and SP (r13)

9.lr and LR (r14)

10.pc and PC (программный счетчик, r15).

Переменные и костанты
В ARM ассемблере, как и любом (практически) другом языке программирования могут использоваться переменные и константы. Они разделяются на такие типы:
  • Числовые
  • Логические
  • Строковые
Числовые переменные инициализируются так:
a SETA 100; создается числовая переменная «a» с значением 100.
Строковые переменные:
improb SETS «literal»; создается переменная improb с значение «literal». ВНИМАНИЕ! Значение переменной не может превышать 5120 символов.
В логических переменных соответственно используются значения TRUE и FALSE.
Примеры инструкций ARM ассемблера
В данной таблице я собрал основные инструкции, которая потребуется для дальнейшей разработки (на самом базовом этапе:):

Чтобы закрепить использование основных инструкций давайте напишем несколько простых примеров, но сначала нам понадобится arm toolchain. Я работаю в Linux поэтому выбрал: frank.harvard.edu/~coldwell/toolchain (arm-unknown-linux-gnu toolchain). Ставится он проще простого, как и любая другая программа на Linux. В моем случае (Russian Fedora) понадобилось только установить rpm пакеты с сайта.
Теперь пришло время написать простейший пример. Программа будет абсолютно бесполезной, но главное, что будет работать:) Вот код, который я вам предлагаю:
start: @ Необязательная строка, обозначающая начало программы mov r0, #3 @ Грузим в регистр r0 значение 3 mov r1, #2 @ Делаем тоже самое с регистром r1, только теперь с значением 2 add r2, r1, r0 @ Складываем значения r0 и r1, ответ записываем в r2 mul r3, r1, r0 @ Умножаем значение регистра r1 на значение регистра r0, ответ записываем в r3 stop: b stop @ Строка завершения программы
Компилируем программу до получения.bin файла:
/usr/arm/bin/arm-unknown-linux-gnu-as -o arm.o arm.s /usr/arm/bin/arm-unknown-linux-gnu-ld -Ttext=0x0 -o arm.elf arm.o /usr/arm/bin/arm-unknown-linux-gnu-objcopy -O binary arm.elf arm.bin
(код в файле arm.s, а toolchain в моем случае лежит в директории /usr/arm/bin/)
Если все прошло успешно, у вас будет 3 файла: arm.s (собственно код), arm.o, arm.elf, arm.bin (собственно исполняемая программа). Для того, чтобы проверить работу программы не обязательно иметь собственное arm устройство. Достаточно установить QEMU. Для справки:

QEMU - свободная программа с открытым исходным кодом для эмуляции аппаратного обеспечения различных платформ.

Включает в себя эмуляцию процессоров Intel x86 и устройств ввода-вывода. Может эмулировать 80386, 80486, Pentium, Pentium Pro, AMD64 и другие x86-совместимые процессоры; PowerPC, ARM, MIPS, SPARC, SPARC64, m68k - лишь частично.

Работает на Syllable, FreeBSD, FreeDOS, Linux, Windows 9x, Windows 2000, Mac OS X, QNX, Android и др.

Итак, для эмуляции arm понадобится qemu-system-arm. Этот пакет есть в yum, так что тем, у кого Fedora, можно не заморачиваться и просто выполнить комманду:
yum install qemu-system-arm

Далее надо запустить эмулятор ARM, так, чтобы он выполнил нашу программу arm.bin. Для этого создадим файл flash.bin, который будет флэш памятью для QEMU. Сделать это очень просто:
dd if=/dev/zero of=flash.bin bs=4096 count=4096 dd if=arm.bin of=flash.bin bs=4096 conv=notrunc
Теперь грузим QEMU с полученой flash памятью:
qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
На выходе вы получите что-то вроде этого:

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
QEMU 0.15.1 monitor - type "help" for more information
(qemu)

Наша программа arm.bin должна была изменить значения четырех регистров, следовательно для проверки правильности работы давайте посмотрим на эти самые регистры. Делается это очень простой коммандой: info registers
На выходе вы увидите все 15 ARM регистров, при чем у четырех из них будут измененные значения. Проверьте:) Значения регистров совпадают с теми, которые можно ожидать после исполнения программы:
(qemu) info registers R00=00000003 R01=00000002 R02=00000005 R03=00000006 R04=00000000 R05=00000000 R06=00000000 R07=00000000 R08=00000000 R09=00000000 R10=00000000 R11=00000000 R12=00000000 R13=00000000 R14=00000000 R15=00000010 PSR=400001d3 -Z-- A svc32

P.S. В этой статье я постарался описать основы программирования на ARM ассемблер. Надеюсь вам понравилось! Этого хватит для того, чтобы далее углубляться в дебри этого языка и писать на нем программы. Если все получится, буду писать дальше о том, что узнаю сам. Если есть ошибки, прошу не пинать, так как я новичок в ассемблере.

В данном разделе приводится описание наборов инструкций процессора ARM7TDMI.

4.1 Краткое описание формата

В данном разделе представлено краткое описание наборов инструкций ARM и Thumb.

Ключ к таблицам наборов инструкций представлен в таблице 1.1.

Процессор ARM7TDMI выполнен на основе архитектуры ARMv4T. Более полное описание обоих наборов инструкций представлено в "ARM Architecture Reference Manual".

Таблица 1.1. Ключ к таблицам

Форматы набора инструкций ARM показаны на рисунке 1.5.

Более детальная информация относительно форматов набора инструкций ARM приведена в "ARM Architectural Reference Manual".

Рисунок 1.5. Форматы набора инструкций ARM

Некоторые коды инструкций не определены, но они не вызывают поиска неопределенных инструкций, например, инструкция умножения с битом 6 измененным к 1. Запрещается использовать такие инструкции, т.к. в будущем их действие может быть изменено. Результат выполнения данных кодов инструкций в составе процессора ARM7TDMI непредсказуем.

4.2 Краткое описание инструкций ARM

Набор инструкций ARM представлен в таблице 1.2.

Таблица 1.2. Краткое преставление инструкций ARM

Операции Синтаксис Ассемблера
Пересылка Пересылка MOV {cond}{S} Rd,
Пересылка NOT MVN {cond}{S} Rd,
Пересылка SPSR в регистр MRS {cond} Rd, SPSR
Пересылка CPSR в регистр MRS {cond} Rd, CPSR
Пересылка регистра SPSR MSR {cond} SPSR{field}, Rm
Пересылка CPSR MSR {cond} CPSR{field}, Rm
Пересылка константы во флаги SPSR MSR {cond} SPSR_f, #32bit_Imm
Пересылка константы во флаги CPSR MSR {cond} CPSR_f, #32bit_Imm
Арифметические Сложение ADD {cond}{S} Rd, Rn,
Сложение с переносом ADC {cond}{S} Rd, Rn,
Вычитание SUB {cond}{S} Rd, Rn,
Вычитание с переносом SBC {cond}{S} Rd, Rn,
Вычитание обратного вычитания RSB {cond}{S} Rd, Rn,
Вычитание обратного вычитания с переносом RSC {cond}{S} Rd, Rn,
Умножение MUL {cond}{S} Rd, Rm, Rs
Умножение-накопление MLA {cond}{S} Rd, Rm, Rs, Rn
Умножение длинных беззнаковых чисел UMULL
Умножение - беззнаковое накопление длинных значений UMLAL {cond}{S} RdLo, RdHi, Rm, Rs
Умножение знаковых длинных SMULL {cond}{S} RdLo, RdHi, Rm, Rs
Умножение - знаковое накопление длинных значений SMLAL {cond}{S} RdLo, RdHi, Rm, Rs
Сравнение CMP {cond} Rd,
Сравнение отрицательное CMN {cond} Rd,
Логические Проверка TST {cond} Rn,
Проверка на эквивалентность TEQ {cond} Rn,
Лог. И AND {cond}{S} Rd, Rn,
Искл. ИЛИ EOR {cond}{S} Rd, Rn,
ORR ORR {cond}{S} Rd, Rn,
Сброс бита BIC {cond}{S} Rd, Rn, >
Переход Переход {cond} label
Переход по ссылке {cond} label
Переход и изменение набора инструкций {cond} Rn
Чтение слова LDR {cond} Rd,
LDR {cond}T Rd,
байта LDR {cond}B Rd,
LDR {cond}BT Rd,
байта со знаком LDR {cond}SB Rd,
полуслова LDR {cond}H Rd,
полуслова со знаком LDR {cond}SH Rd,
операции с несколькими блоками данных -
  • с предварительным инкрементом
  • LDM {cond}IB Rd{!}, {^}
  • с последующим инкрементом
  • LDM {cond}IA Rd{!}, {^}
  • с предварительным декрементом
  • LDM {cond}DB Rd{!}, {^}
  • с последующим декрементом
  • LDM {cond}DA Rd{!}, {^}
  • операция над стеком
  • LDM {cond} Rd{!},
  • операция над стеком и восстановление CPSR
  • LDM {cond} Rd{!}, ^
    операция над стеком с регистрами пользователя LDM {cond} Rd{!}, ^
    Запись слова STR {cond} Rd,
    слова с преимуществом режима пользователя STR {cond}T Rd,
    байта STR {cond}B Rd,
    байта с преимуществом режима пользователя STR {cond}BT Rd,
    полуслова STR {cond}H Rd,
    операции над несколькими блоками данных -
  • с предварительным инкрементом
  • STM {cond}IB Rd{!}, {^}
  • с последующим инкрементом
  • STM {cond}IA Rd{!}, {^}
  • с предварительным декрементом
  • STM {cond}DB Rd{!}, {^}
    o с последующим декрементом STM {cond}DA Rd{!}, {^}
  • операция над стеком
  • STM {cond} Rd{!},
  • операция над стеком с регистрами пользователя
  • STM {cond} Rd{!}, ^
    Обмен слов SWP {cond} Rd, Rm,
    байт SWP {cond}B Rd, Rm,
    Сопроцессор Операция над данными CDP {cond} p, , CRd, CRn, CRm,
    Пересылка в ARM-регистр из сопроцессора MRC {cond} p, , Rd, CRn, CRm,
    Пересылка в сопроцессор из ARM-регистра MCR {cond} p, , Rd, CRn, CRm,
    Чтение LDC {cond} p, CRd,
    Запись STC {cond} p, CRd,
    Программное прерывание SWI 24bit_Imm

    Подробно ознакомиться с системой команд в режиме ARM можно .

    Режимы адресации

    Режимы адресации - процедуры, которые используются различными инструкциями для генерации значений, используемых инструкциями. Процессор ARM7TDMI поддерживает 5 режимов адресации:

    • Режим 1 - Сдвиговые операнды для инструкций обработки данных.
    • Режим 2 - Чтение и запись слова или беззнакового байта.
    • Режим 3 - Чтение и запись полуслова или загрузка знакового байта.
    • Режим 4 - Множественные чтение и запись.
    • Режим 5 - Чтение и запись сопроцессора.

    Режимы адресации с указанием их типов и мнемонических кодов представлены в таблице 1.3.

    Таблица 1.3. Режимы адресации

    Режим адресации Тип или режим адресации Мнемонический код или тип стека
    Режим 2 Константа смещения
    Регистр смещения
    Масштабный регистр смещения
    Предварительное индексированное смещение -
    Константа !
    Регистр !
    Масштабный регистр !
    !
    !
    !
    !
    -
    Константа , #+/-12bit_Offset
    Регистр , +/-Rm
    Масштабный регистр
    Режим 2, привилегированный Константа смещения
    Регистр смещения
    Масштабный регистр смещения
    Смещение с последующим индексированием -
    Константа , #+/-12bit_Offset
    Регистр , +/-Rm
    Масштабный регистр , +/-Rm, LSL #5bit_shift_imm
    , +/-Rm, LSR #5bit_shift_imm
    , +/-Rm, ASR #5bit_shift_imm
    , +/-Rm, ROR #5bit_shift_imm
    Режим 3, > Константа смещения
    !
    Последующее индексирование , #+/-8bit_Offset
    Регистр
    Предварительное индексирование !
    Последующее индексирование , +/-Rm
    Режим 4, чтение IA, последующий инкремент FD, full descending
    ED, empty descending
    DA, последующий декремент FA, full ascending
    DB предварительный декремент EA, empty ascending
    Режим 4, запись IA, последующий инкремент FD, full descending
    IB, предварительный инкремент ED, empty descending
    DA, последующий декремент FA, full ascending
    DB предварительный декремент EA, empty ascending
    Режим 5, передача данных сопроцессора Константа смещения
    Предварительное индексирование !
    Последующее индексирование , #+/-(8bit_Offset*4)

    Операнд 2

    Операнд является частью инструкции, которая ссылается на данные или периферийное устройство. Операнды 2 представлены в таблице 1.4.

    Таблица 1.4. Операнд 2

    Поля представлены в таблице 1.5.

    Таблица 1.5. Поля

    Поля условий

    Поля условий представлены в таблице 1.6.

    Таблица 1.6. Поля условий

    Тип поля Суффикс Описание Условие
    Условие {cond} EQ Равно Z=1
    NE Неравно Z=0
    CS Беззнаковое больше или равно C=1
    CC Беззнаковое меньше C=0
    MI Отрицательное N=1
    PL Положительное или ноль N=0
    VS Переполнение V=1
    VC Нет переполнения V=0
    HI Беззнаковое больше C=1, Z=0
    LS Беззнаковое меньше или равно C=0, Z=1
    GE Больше или равно N=V (N=V=1 или N=V=0)
    LT Меньше NV (N=1 и V=0) или (N=0 и V=1)
    GT Больше Z=0, N=V (N=V=1 или N=V=0)
    LE Меньше или равно Z=0 или NV (N=1 и V=0) или (N=0 и V=1)
    AL Всегда истинный флаги игнорируются

    4.3 Краткое описание набора инструкций Thumb

    Форматы набора инструкций Thumb показаны на рисунке 1.6. Более подробная информация по форматам наборов инструкций ARM приведена "ARM Architectural Reference Manual".


    Рисунок 1.6. Форматы набора инструкций Thumb

    Набор инструкций Thumb представлен в таблице 1.7.

    Таблица 1.7. Краткое описание набора инструкций Thumb

    Операция Синтаксис Ассемблера
    Пересылка (копирование) константы MOV Rd, #8bit_Imm
    старшего в младший MOV Rd, Hs
    младшего в старший MOV Hd, Rs
    старшего в старший MOV Hd, Hs
    Арифметические сложение ADD Rd, Rs, #3bit_Imm
    прибавить младший к младшему ADD Rd, Rs, Rn
    прибавить старший к младшему ADD Rd, Hs
    прибавить младший к старшему ADD Hd, Rs
    прибавить старший к старшему ADD Hd, Hs
    сложение с константой ADD Rd, #8bit_Imm
    прибавить значение к SP ADD SP, #7bit_Imm ADD SP, #-7bit_Imm
    сложение с учетом переноса ADC Rd, Rs
    вычитание SUB Rd, Rs, Rn SUB Rd, Rs, #3bit_Imm
    вычитание константы SUB Rd, #8bit_Imm
    вычитание с переносом SBC Rd, Rs
    инверсия знака NEG Rd, Rs
    умножение MUL Rd, Rs
    сравнить младший с младшим CMP Rd, Rs
    сравнить младший и старший CMP Rd, Hs
    сравнить старший и младший CMP Hd, Rs
    сравнить старший и старший CMP Hd, Hs
    сравнить отрицательные CMN Rd, Rs
    сравнить с константой CMP Rd, #8bit_Imm
    Логические И AND Rd, Rs
    Искл. ИЛИ EOR Rd, Rs
    ИЛИ ORR Rd, Rs
    Сброс бита BIC Rd, Rs
    Пересылка NOT MVN Rd, Rs
    Тестирование бит TST Rd, Rs
    Сдвиг/вращение Логический сдвиг влево LSL Rd, Rs, #5bit_shift_imm LSL Rd, Rs
    Логический сдвиг вправо LSR Rd, Rs, #5bit_shift_imm LSR Rd, Rs
    Арифметический сдвиг вправо ASR Rd, Rs, #5bit_shift_imm ASR Rd, Rs
    Вращение вправо ROR Rd, Rs
    Переход условные переходы -
    BEQ label
    BNE label
    BCS label
    BCC label
    BMI label
    BPL label
    BVS label
    BVC label
  • C=1, Z=0
  • BHI label
  • C=0, Z=1
  • BLS label
  • N=1, V=1 или N=0, V=0
  • BGE label
  • N=1, V=0 или N=0, V=1
  • BLT label
  • Z=0 и ((N или V=1) или (N или V=0))
  • BGT label
  • Z=1 или ((N=1 или V=0) или (N=0 и V=1))
  • BLE label
    Безусловный переход B label
    Длинный переход по ссылке BL label
    Опциональное изменение состояния -
  • по адресу в мл. регистре
  • BX Rs
  • по адресу в ст. регистре
  • BX Hs
    Чтение с константой смещения -
  • слова
  • LDR Rd,
  • полуслова
  • LDRH Rd,
  • байта
  • LDRB Rd,
    с регистром смещения -
  • слова
  • LDR Rd,
  • полуслова
  • LDRH Rd,
  • знакового полуслова
  • LDRSH Rd,
    LDRB Rd,
  • знакового байта
  • LDRSB Rd,
    относительно счетчика программы PC LDR Rd,
    относительно указателя стека SP LDR Rd,
    Адрес -
  • с помощью PC
  • ADD Rd, PC, #10bit_Offset
  • с помощью SP
  • ADD Rd, SP, #10bit_Offset
    Множественное чтение LDMIA Rb!,
    Запись с константой смещения -
  • слова
  • STR Rd,
  • полуслова
  • STRH Rd,
  • байта
  • STRB Rd,
    с регистром смещения -
  • слова
  • STR Rd,
  • полуслова
  • STRH Rd,
  • байта
  • STRB Rd,
    относительно SP STR Rd,
    Множественная запись STMIA Rb!,
    Помещение/ извлечение из стека Поместить регистры в стек PUSH
    Поместить LR и регистры в стек PUSH
    Извлечь регистры из стека POP
    Извлечь регистры и PC из стека POP
    Программное прерывание - SWI 8bit_Imm

    Процессоры CISC выполняют за одну команду довольно сложные операции, включая арифметические и логические операции над содержимым ячеек памяти. Команды CISC процессора могут иметь разную длину.

    Напротив, RISC имеет относительно простую систему команд с четким делением по типу операции:

    • работа с памятью (считывание из памяти в регистры или запись из регистров в память),
    • обработка данных в регистрах (арифметические, логические, сдвиги данных влево/вправо или ротация бит в регистре),
    • команды условных или безусловных переходов на другие адреса.

    Как правило (но не всегда, и только при условии попадания кода программы в память кэш контроллера) одна команда исполняется один такт процессора. Длина команды процессора ARM фиксированная - 4 байта (одно компьютерное слово). Вообще-то современный процессор ARM может переходить в другие режимы работы, например, в режим THUMB, когда длина команды становится 2 байта. Это позволяет сделать код более компактным. Однако в этой статье мы не рассматриваем этот режим, так как в процессоре Amber ARM v2a он не поддерживается. По этой же причине не будем рассматривать такие режимы как Jazelle (оптимизирован для исполнения Java кода) и не будем рассматривать команды NEON - команды операций над множественными данными. Все-таки мы изучаем чистую систему команд ARM.

    Регистры процессора ARM.

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

    • режим приложения (USR, user mode),
    • режим супервизора или режим операционной системы (SVC, supervisor mode),
    • режим обработки прерывания (IRQ, interrupt mode) и
    • режим обработки «срочного прерывания» (FIRQ, fast interrupt mode).

    То есть, например, при возникновении прерывания процессор сам переходит к адресу программы обработчика прерываний и сам автоматически «переключает» банки регистров.

    Процессоры ARM более старших версий кроме вышеперечисленных режимов работы имеют еще дополнительные режимы:

    • Abort (используется для обработки исключений доступа к памяти),
    • Undefined (используется для реализации сопроцессора программным способом) и
    • режим привелигированных задач операционной системы System.

    В процессоре Amber ARM v2a этих дополнительных трех режимов нет.

    Для Amber ARM v2a набор регистров можно представить следующим образом:

    Регистры r0-r7 одни и те же для всех режимов.
    Регистры r8-r12 общие только для режимов USR, SVC, IRQ.
    Регистр r13 - является указателем стека. Он во всех режимах свой.
    Регистр r14 - регистр возврата из подпрограммы так же во всех режимах свой.
    Регистр r15 является указателем на исполняемые команды. Он общий для всех режимов.

    Видно, что режим FIRQ самый обособленный, у него больше всего своих собственных регистров. Это сделано для того, чтобы какое-то очень критичное прерывание можно было бы обрабатывать не сохраняя регистры в стек, не теряя на это время.

    Особенное внимание нужно уделить регистру r15 , он же pc (Program Counter ) - указатель на исполняемые команды. Над его содержимым можно выполнять разные арифметические и логические операции, тем самым исполнение программы будет переходить на другие адреса. Однако, именно для процессора ARM v2a, реализованного в системе Amber есть некоторые тонкости в интерпретации битов этого регистра.

    Дело в том, что в этом процессоре в регистре r15 (pc ) кроме собственно указателя на исполняемые команды содержится следующая информация:

    Биты 31:28 - флаги результата выполнения арифметической или логической операции
    Биты 27 - маска IRQ прерывания, прерывания запрещены, когда бит установлен.
    Биты 26 - маска FIRQ прерывания, быстрые прерывания запрещены, когда бит установлен.
    Биты 25:2 - собственно указатель на команды программы занимает только 26 бит.
    Биты 1:0 - текущий режим работы процессора.
    3 - Supervisor
    2 - Interrupt
    1 - Fast Interrupt
    0 - User

    В более старших процессорах ARM все флаги и служебные биты расположены в отдельных регистрах Current Program Status Register (cpsr ) и Saved Program Status Register (spsr ), для доступа к которым есть отдельные специальные команды. Это сделано для того, что бы расширить доступное адресное пространство для программ.

    Одна из трудностей освоения ассемблера ARM - это альтернативные имена некоторых регистров. Так, как выше было сказано, r15 - это тот же pc . Еще есть r13 - это тот же sp (Stack Pointer ), r14 - это lr (Link Register ) - регистр адреса возврата из процедуры. Кроме этого, r12 - это тот же самый ip (Intra-Procedure -call scratch register), используется компиляторами C особым образом для доступа к параметрам в стеке. Такое альтернативное именование иногда сбивает с толку, когда смотришь в чужой код программы - там встречаются и те и эти обозначения регистров.

    Особенности исполнения кода.

    Во многих типах процессороров (например x86) по условию может выполняться только переход на другой адрес программы. В ARM это не так. Каждая команда процессора ARM может быть выполнена или не выполнена по условию. Это позволяет минимизировать количество переходов по программе и следовательно эффективнее использовать конвейер (pipeline) процессора.

    Ведь что такое pipeline? Одна команда процессора сейчас выбирается из кода программы, предыдущяя уже декодируется, а пред-предыдущая уже исполняется. Это в случае 3х стадийного конвейера процессора Amber A23, который мы используем в нашем проекте для платы Марсоход2Марсоход2 . Модификация процессора Amber A25 имеет 5-ти стадийный конвейер, он еще более эффективный. Но, есть одно большое НО. Команды перехода вынуждают процессор очищать pipeline и наполнять его заново. Таким образом, новая команда выбирается, но еще нечего декодировать и тем более сразу нечего исполнять. Эффективность выполнения кода при частых переходах падает. В современных процессорах есть всякие механизмы предсказания переходов, которые как-то оптимизируют наполнение конвейера, но в нашем процессоре этого нет. В любом случае, ARM поступила мудро, сделав возможным исполнение каждой команды условным.

    В процессоре ARM, в команде любого типа четыре бита условия исполнения команды закодированы в старших четырех битах кода команды:

    Всего в процессоре 4 флага условия:
    . Negative - результат операции получился отрицательным,
    . Zero - результат равен нулю,
    . Carry - при выполнении операции с беззнаковыми чисоами произошел перенос,
    . oVerflow - при выполнении операции со знаковыми числами произошло переполнение, результат не помещается в регистр}

    Эти 4 флага формируют множество возможных комбинаций условия:

    Код Суффикс Значение Флаги
    4"h0 eq Equal Z set
    4"h1 ne Not equal Z clear
    4"h2 cs / hs Carry set / unsigned higher or same C set
    4"h3 cc / lo Carry clear / unsigned lower C clear
    4"h4 mi Minus / negative N set
    4"h5 pl Plus / positive or zero N clear
    4"h6 vs Overflow V set
    4"h7 vc No overflow V clear
    4"h8 hi Unsigned higher C set and Z clear
    4"h9 ls Unsigned lower or same C clear or Z set
    4"ha ge Signed greater than or equal N == V
    4"hb lt Signed less than N != V
    4"hc gt Signed greater than Z == 0,N == V
    4"hd le Signed less than or equal Z == 1 or N != V
    4"he al Always (unconditional)
    4"hf - Invalid condition

    Теперь из этого следует еще одна сложность изучения команд процессора ARM - множество суффиксов, которые могут быть добавлены к коду команды. Например, сложение при условии, что флаг Z установлен - это команда addeq как add + суффикс eq . Переход на подпрограмму в случае, если флаг N=0 - это blpl как bl + суффикс pl .

    Флаги { Negative, Zero, Carry, oVerflow } устанавливаются то же не всегда при арифметических или логических операциях, как это бывает скажем в x86 процессоре, а только, когда захочет программист. Для этого есть еще один суффикс к мнемонике команд: «s » (в коде команды кодируется битом 20). Таким образом, команда сложения add не меняет флагов, а команда adds меняет флаги. А может еще быть условная команда сложения, но которая меняет флаги. Например: addgts . Понятно, что число возможных сочетаний названий команд с разными суффиксами условного выполнения и установки флагов делает ассемблерный код ARM процессора весьма своеобразным и трудно читаемым. Однако со временем к этому привыкаешь и начинаешь понимать этот текст.

    Арифметические и логические операции (Data Processing).

    Процессор ARM может выполнять различные арифметические и логические операции.

    Собственно четырехбитный код операции {Opcode} содержится в битах команды процессора.

    Любая операция выполняется над содержимым регистра и так называемым shifter_operand . Результат операции помещается в регистр . Четырехбитные Rn и Rd - это индексы регистров в активном банке процессора.

    В зависимости от бита I 25 shifter_operand трактуется либо как числовая константа, либо как индекс второго регистра операнда и даже операция сдвига над значением второго операнда.

    Простые примеры команд ассемблера будут выглядеть, например, вот так:

    add r0,r1,r2 @ поместить в регистр r0 сумму значений регистров r1 и r2
    sub r5,r4,#7 @ поместить в регистр r5 разность (r4-7)

    Выполняемые операции кодируются следующим образом:

    4"h0 and Логическое И Rd:= Rn AND shifter_operand
    4"h1 eor Логическаое исключающее ИЛИ Rd:= Rn XOR shifter_operand
    4"h2 sub Арифметическое вычитание Rd:= Rn - shifter_operand
    4"h3 rsb Арифметическое обратное вычитание Rd:= shifter_operand - Rn
    4"h4 add Арифметическое сложение Rd:= Rn + shifter_operand
    4"h5 adc Арифметическое сложение плюс флаг переноса Rd:= Rn + shifter_operand + Carry Flag
    4"h6 sbc Арифметическое вычитание с переносом Rd:= Rn - shifter_operand - NOT(Carry Flag)
    4"h7 rsc Арифметическое обратное вычитание с переносом Rd:= shifter_operand - Rn - NOT(Carry Flag)
    4"h8 tst Логическое И, но без запоминания результата, изменяются только флаги Rn AND shifter_operand S bit always set
    4"h9 teq Логическое исключающее ИЛИ, но без запоминания результата, изменяются только флаги Rn EOR shifter_operand
    S bit always set
    4"ha cmp Сравнение, вернее арифметическое вычитание без запоминания результата, изменяются только флаги Rn - shifter_operand S bit always set
    4"hb cmn Сравнение инверсного, вернее арифметическое сложение без запоминания результата, изменяются только флаги Rn + shifter_operand S bit always set
    4"hc orr Логическое ИЛИ Rd:= Rn OR shifter_operand
    4"hd mov Копирование значения Rd:= shifter_operand (no first operand)
    4"he bic Сброс битов Rd:= Rn AND NOT(shifter_operand)
    4"hf mvn Копирование инверсногозначения Rd:= NOT shifter_operand (no first operand)

    Barrel shifter.

    В процессоре ARM есть специальная схема “barrel shifter” которая позволяет один из операндов сдвинуть или развернуть на заданное число бит перед любой арифметической или логической операцией. Это довольно интересная особенность процессора, которая позволяет создавать очень эффективный код.

    Например:

    @ умножение на 9 - это умножение числа на 8
    @ путем сдвига влево на 3 бита плюс еще число
    add r0, r1, r1, lsl #3 @ r0= r1+(r1<<3) = r1*9

    @ умножение на 15 - это умножение на 16 минус число
    rsb r0, r1, r1, lsl #4 @ r0= (r1<<4)-r1 = r1*15

    @ доступ к таблице 4-х байтовых слов, где
    @ r1 - это базовый адрес таблицы
    @ r2 - это индекс элемента в таблице
    ldr r0,

    Кроме логического сдвига влево lsl есть еще логический сдвиг вправо lsr и арифметический сдвиг вправо asr (сдвиг с сохранением знака числа, старший бит размножается слева одновременно со сдвигом).

    Еще есть ротация бит ror - биты выдвигаются вправо и те, что выдвиннуты - задвигаются слева.
    Есть сдвиг на один бит через флаг C - это команда rrx . Значение регистра сдвигается вправо на один бит. Слева в старший разряд регистра загружается флаг C

    Сдвиг может осуществляться не на фиксированное число-константу, а по значению третьего регистра-операнда. Например:

    add r0, r1, r1, lsr r3 @ это r0 = r1 + (r1>>r3);
    add r0, r0, r1, lsr r3 @ это r0 = r0 + (r1>>r3);

    Таким образом, shifter_operand это то, что мы описываем в командах ассемблера, например, как «r1, lsr r3 » или «r2, lsl #5 ».

    Самое интересное, что использование сдвигов в операциях ничего не стоит. На эти сдвиги (обычно) не тратится дополнительных тактов и это очень хорошо для производительности системы.

    Использование числовых операндов.

    Арифметические или логические операции могут использовать в качестве второго операнда не только содержимое регистра, но и числовую константу.

    К сожалению, здесь существует одно важное ограничение. Поскольку все команды имеют фиксированную длину 4 байта (32 бита), то закодировать в ней «любое» число не получится. В коде операции и так 4 бита заняты кодом условия выполнения {Cond} , 4 бита на сам код операции {Opcode} , потом, 4 бита - регистр приемника Rd , и еще 4 бита - регистр первого операнда Rn , плюс еще разные флаги I 25 (как раз обозначает числовую константу в коде операции) и S 20 (установка флагов после операции). Итого, на возможную константу остается всего 12 бит, так называемый shifter_operand - мы это видели выше. Поскольку 12-ю битами можно закодировать числа только в узком диапазоне разработчики процессора ARM решили сделать кодирование константы следующим образом. Двенадцать бит shifter_operand разбиты на две части: четырехбитный показатель вращения encode_imm и собственно восьмибитное числовое значение imm_8 .

    В процессоре ARM константа определяется восьмибитным числом внутри 32-х битного числа, развернутым вправо на четное число бит. То есть:

    imm_32 = imm_8 ROR (encode_imm *2)

    Получилось довольно мудрено. Получается, что не каждое число константу можно использовать в командах ассемблера.

    Можно написать

    add r0, r2, #255 @ константа в десятичном виде
    add r0, r3, #0xFF @ константа в шестнадцатеричном виде

    так как 255 находится в диапазоне 8 бит. Эти команды будут скомпилированны вот так:

    0: e28200ff add r0, r2, #255 ; 0xff
    4: e28300ff add r0, r3, #255 ; 0xff

    И даже можно написать

    add r0, r4, #512
    add r0, r5, 0x650000

    Скомпилированный код получится вот такой:

    0: e2840c02 add r0, r4, #512 ; 0x200
    4: e2850865 add r0, r5, #6619136 ; 0x650000

    В этом случае, само число 512, конечно, не помещается в байт. Но зато мы представляем себе его в шестнадцатеричном виде 32’h00000200 и видим, что это 2 развернутая вправо на 24 бита (1 ror 24). Коэффициент вращения в два раза меньше, чем 24, то есть 12. Вот и получается shifter_operand = { 4’hc , 8’h02 } - это двенадцать младших бит команды. Так же и с числом 0x650000. Для него shifter_operand = { 4’h8, 8’h65 }.

    Понятно, что нельзя написать

    add r0, r1,#1234567

    или нельзя написать

    mov r0, #511

    так как здесь число не получается представить в виде imm_8 и encode_imm - фактора вращения. Компилятор ассемблера будет выдавать ошибку.

    Что же делать, когда константа не может быть прямо закодирована в shifter_operand ? Придется делать всякие ухищрения.
    Например, можно сперва загрузить в свободный регистр число 512, а потом вычесть единицу:

    mov r0, #511
    sub r0,r0,#1

    Второй способ загрузить в регистр специфическое число - считать его из специально зарезервированной переменной находящейся в памяти:

    ldr r7,my_var
    .....
    my_var: .word 0x123456

    Самый же простой способ написать вот так:

    ldr r2,=511

    В этом случае (обратите внимание на знак «=») если константа может быть представлена как imm_8 и encode_imm , если может быть вписана в 12 бит shifter_operand , то компилятор ассемблера автоматически скомпилирует ldr в команду mov . А вот если число не может быть так представлено, то компилятор сам зарезервирует в программе ячейку памяти для этой константы, и сам задаст этой ячейке памяти имя и скомпилирует команду в ldr .

    Вот я написал вот так:

    ldr r7,my_var
    ldr r8,=511
    ldr r8,=1024
    ldr r9,=0x3456
    ........
    My_var: .word 0x123456

    После компиляции получил вот что:

    18: e59f7030 ldr r7, ; 50
    1c: e59f8030 ldr r8, ; 54
    20: e3a08b01 mov r8, #1024 ; 0x400
    24: e59f902c ldr r9, ; 58
    .............
    00000050 :
    50: 00123456 .word 0x00123456
    54: 000001ff .word 0x000001ff
    58: 00003456 .word 0x00003456

    Обратите внимание, что компилятор использует адресацию ячеек памяти относительно регистра pc (он же r15 ).

    Чтение ячейки памяти и запись регистра в память.

    Как я уже написал выше, процессор ARM может выполнять арифметические или логические операции только над содержимым регистров. Данные для операций нужно читать из памяти и результат операций записывать опять в память. Для этого существуют специальные команды: ldr (вероятно от сочетания «LoaD Register ») для чтения и str (наверное «STore Regi ster ») для записи.

    Казалось бы - всего две команды, но на самом деле у них есть много вариаций. Достаточно посмотреть на способы кодирования команд ldr /str процессора Amber ARM, чтобы увидеть, как много вспомогательных битов-флажков L 20 , W 21 , B 22 , U 23 , P 24 , I 25 - и они определяют конкретное поведение команды:

    • Бит L 20 определяет запись или чтение. 1 - ldr , чтение, 0 - str , запись.
    • Бит B 22 определяет чтение/запись 32-х битного слова или 8-ми битного байта. 1 - значит операция с байтом. При чтении байта в регистр старшие биты регистра обнуляются.
    • Бит I 25 определяет использование поля Offset . Если I 25 ==0, то Offset интерпретируется как числовое смещение, которое нужно либо прибавить к базовому адресу из регистра или отнять. А вот прибавлять или отнимать зависит от бита U 23 .

    {Cond} - условие выполнения операции. Интерпретируется так же, как и для логических/арифметических команд - чтение или запись могут быть условными.

    Таким образом, в ассемблерном тексте можно написать вот такое:

    ldr r1, @ в регистр r1 читать слово по адресу из регистра r0
    ldrb r1, @ в регистр r1 читать байт по адресу из регистра r0
    ldreq r2, @ условное чтение слова
    ldrgtb r2, @ условное чтение байта
    ldr r3, @ чтение слова по адресу 8 относительно адреса из регистра r4
    ldr r4, @ чтение слова по адресу -16 относительно адреса из регистра r5

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

    0: e5901000 ldr r1,
    4: e5d01000 ldrb r1,
    8: 05912000 ldreq r2,
    c: c5d12000 ldrbgt r2,
    10: e5943008 ldr r3,
    14: e5154010 ldr r4,

    В приведенном выше примере я использую только ldr , но и str используется примерно так же.

    Существуют режимы пре-индексного и пост-индексного доступа к памяти с обратной записью. В этих режимах указатель доступа к памяти обновляется до или после выполнения команды. Если вы знакомы с языком программирования C, то вам известны конструкции доступа по указателям вроде (*psource++; ) или ( a=*++psource; ). В процессоре ARM этот режим доступа в памяти как раз и реализован. При выполнении команды чтения обновляется сразу два регистра - регистр приемник получает считанное из памяти значение и значение в регистре-указателе на ячейку памяти перемещается вперед или назад.

    Запись этих команд, на мой взгляд, несколько нелогична. Нужно долго привыкать.

    ldr r3, ! @ psrc++; r3 = *psrc;
    ldr r3, , #4 @ r3 = *psrc; psrc++;

    Первая команда ldr сперва увеличивает указатель, потом выполняет чтение. Вторая команда сперва выполняет чтение, потом увеличивает указатель. Значение указателя psrc находится в регистре r0 .

    Все рассмотренные выше примеры были для случая, когда бит I 25 в коде команды был сброшен. Но ведь он еще может быть установленным! Тогда в значении поля Offset будет не числовая константа, а уже третий регистр, учавствующий в операции. Причем значение третьего регистра еще может быть предварительно сдвинуто!

    Вот примеры возможных вариаций кода:

    0: e7921003 ldr r1, @ адрес для чтения - сумма значений из регистров r2 и r3
    4: e7b21003 ldr r1, ! @ то же самое, но после чтения r2 будет увеличен на значение из r3
    8: e6932004 ldr r2, , r4 @ сперва будет чтение по адресу r3, а потом r3 увеличится на r4
    c: e7943185 ldr r3, @ адрес для чтения r4+r5*8
    10: e7b43285 ldr r3, ! @ адрес для чтения r4+r5*32, после чтения r4 будет установлен в значение этого адреса
    14: e69431a5 ldr r3, , r5, lsr #3 @ адрес для чтения r4, по после исполнения команды r4 будет установлен в r4+r5/8

    Вот такие вариации команд чтения/записи в процессоре ARM v2a.

    В более старших моделях процессоров ARM это разнообразие команд еще больше.
    Это из-за того, что процессор позволяет, например, читать не только слова (32-х битные числа) и байты, но и полуслова (16 бит, 2 байта). Тогда к командам ldr /str добавляется суффикс «h », от слова half-word. Команды будут выглядеть как ldrh или strh . Так же есть команды загрузки полуслов ldrsh или байтов ldrsb интерпретируемых как знаковые числа. В этих случаях старший бит загружаемого полослова или байта размножается в старшие биты целого слова в регистре приемнике. Например, загружая командой ldrsh полуслово 0xff25 в регистре-приемнике получается 0xffffff25 .

    Множественные чтения и запись.

    Команды ldr /str не единственные для доступа к памяти. В процессоре ARM есть еще команды позволяющие выполнять блочную передачу - можно загрузить содержимое нескольких последовательных слов из памяти сразу несколько регистров. Так же можно записать последовательно в память значения нескольких регистров.

    Мнемоники команд блочной передачи начинаются с корня ldm (LoaD Multiple ) или stm (Store Multiple ). А вот дальше, как обычно в ARM, начинается история с суффиксами.

    В общем случае команда выглядит вот так:

    op{cond}{mode} Rd{!}, {Register list}

    Суффикс {Cond} - это понятно, это условие выполнения команды. Суффикс {mode} - это режим передачи, о нем четь позже. Rd - регистр определяющий базовый адрес в памяти для чтения или записи. Восклицательный знак после регистра Rd обозначает, что после операции чтения/записи он будет изменен. Список регистров, которые загружаются из памяти или выгружаются в память - это {Register list} .

    Список регистров задается в в фигурных скобках через запятую или в виде диапазона. Например:

    stm r0,{r3,r1, r5-r8}

    Запись в память будет прозведена не в порядке перечисления. Список просто обозначает какие регистры будут записаны в память и все. В коде команды есть зарезервированные для Register List 16 бит, как раз по числу регистров в банке процессора. Каждый бит в этом поле обозначает какой регистр будет участвовать в операции.

    Теперь о режиме чтения/записи mode. Тут есть где запутаться. Дело в том, что для одного и того же действия могут использоваться разные названия режима.

    Если сделать небольшое лирическое отступление, то нужно рассказать о... стеке. Стек - это способ доступа к данным типа LIFO - Last In First Out (wiki) - последним вошел, первым вышел. Стек широко используется в программировании при вызове процедур и сохранении состояния регистров на входе функций и восстановлении их при выходе, а так же при передаче параметров вызываемым процедурам.

    Стек в памяти бывает, кто бы мог подумать, четырех типов.

    Первый тип - Full Descending . Это когда указатель стека указывает на занятый элемент стека и стек растет в сторону уменьшения адресов. Когда нужно положить слово на стек, то сперва указатель стека уменьшается (Decrement Before ), потом по адресу указателя стека записывается слово. Когда нужно снять компьютерное слово со стека, то по текущему значению указателя стека читается слово, потом указатель перемещается вверх (Increment After ).

    Второй тип - Full Ascending . Стек растет не вниз а в верх, в сторону больших адресов. Указатель так же указывает на занятый элемент. Когда нужно положить слово на стек, то сперва указатель стека увеличивается, потом производится запись слова по указателю (Increment Before ). Когда надо снять со стека, то сперва читаем по указателю стека, ведь он указывает на занятый элемент, потом уменьшается указатель стека (Decrement Afte r).

    Третий тип - Empty Descending . Стек растет вниз, как и в случае с Full Descending , но отличие состоит в том, что указатель стека указывавет на не занятую ячейку. Таким образом, когда нужно положить слово на стек, то сразу делается запись, потом указатель стека уменьшается (Decrement After ). При снятии со стека сперва увеличивают указатель, потом читают (Increment Before ).

    Четвертый тип - Empty Ascending . Надеюсь все понятно - стек растет вверх. Указатель стека указывает на пустой элемент. Положить на стек - это записать слово по адресу указателя стека и увеличить указатель стека (Increment After ). Снять со стека - уменьшить указатель стека и прочитать слово (Decrement Before ).

    Таким образом, при операциях со стеком нужно указатель увеличивать или уменьшать - (Increment/Decrement ) до или после (Before/After ) чтения/записи в память в зависимости от типа стека. В процессорах Intel, например, есть специальные команды для работы со стеком типа PUSH (положить слово на стек) или POP (снять слово со стека). В процессоре ARM специальных команд нет, но используются ldm и stm команды.

    Если реализовывать стек с помощью команд процессора ARM, то получается вот такая картина:

    Зачем одной и той же команде нужно было давать разные имена? Вообще не понимаю... Здесь, конечно, нужно заметить, что стандарт стека для ARM - все же Full Descending.

    Указатель стека в процессоре ARM - это регистр sp или r13 . Это в общем такая договоренность. Конечно, запись stm или чтение ldm могут быть выполнены и с другими базовыми регистрами. Однако нужно помнить, чем регистр sp отличается от других регистров - он в разных режимах работы процессора (USR, SVC, IRQ, FIRQ) может быть свой, ведь там свои банки регистров.

    И еще замечание. Написать в ассемблерном коде ARM строку вроде push {r0-r3} , конечно можно. Только вот на самом деле это будет та же самая команда stmfd sp!,{r0-r3} .

    На последок приведу пример ассемблерного кода и его откомпилированный дизассемблированный текст. Имеем:


    stmfd sp!,{r0-r3}
    stmdb sp!,{r0-r3}
    push {r0-r3}

    @ эти три инструкции одинаковы и делают одно и то же
    pop {r0-r3}
    ldmia sp!,{r0-r3}
    ldmfd r13!,{r0-r3}

    Stmfd r4,{r0-r3,r5,r8}
    stmea r4!,{r0-r3,r7,r9,lr,pc}
    ldm r5,{r0,pc}

    Получаем после компиляции:

    0: e92d000f push {r0, r1, r2, r3}
    4: e92d000f push {r0, r1, r2, r3}
    8: e92d000f push {r0, r1, r2, r3}
    c: e8bd000f pop {r0, r1, r2, r3}
    10: e8bd000f pop {r0, r1, r2, r3}
    14: e8bd000f pop {r0, r1, r2, r3}
    18: e904012f stmdb r4, {r0, r1, r2, r3, r5, r8}
    1c: e8a4c28f stmia r4!, {r0, r1, r2, r3, r7, r9, lr, pc}
    20: e8958001 ldm r5, {r0, pc}

    Переходы в программах.

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

    В процессоре Amber ARM v2a есть всего две команды: b (от слова Branch - ветка, переход) и bl (Branch with Link - переход с сохранением адреса возврата).

    Синтаксис команд очень прост:

    b{cond} label
    bl{cond} label

    Понятно, что любые переходы могут быть условными, то есть в программе могут встретиться вот такие, образованные от корней «b » и «bl » и суффиксов условия {Cond} , странные слова:

    beq, bne, bcs, bhs, bcc, blo, bmi, bpl, bvs, bvc, bhi, bls, bge, bgt, ble, bal, b

    bleq, blne, blcs, blhs, blcc, bllo, blmi, blpl, blvs, blvc, blhi, blls, blge, blgt, blle, blal, bl

    Разнообразие поражает, не правда ли?

    В команде перехода содержится 24-х битное смещение Offset . Адрес перехода вычисляется как сумма текущего значения указателя pc и сдвинутого на 2 бита влево числа Offset , интерпретируемого как знаковое число:

    New pc = pc + Offset*4

    Таким образом, диапазон переходов составляет 32Мб вперед или назад.

    Рассмотрим, что такое переход с сохранением адреса возврата bl . Эта команда используется для вызова подпрограмм. Интересной особенностью этой команды является то, что адрес возврата из процедуры при вызове процедуры сохраняется не в стеке, как у процессоров Интел, а в обычном регистре r14 . Тогда для возврата из процедуры не нужна специальная команда ret , как у тех же процессоров Интел, а можно просто скопировать значение r14 назад в pc . Теперь понятно, почему регистр r14 имеет альтернативное название lr (Link Register ).

    Рассмотрим процедуру outbyte из проекта hello-world для системы на кристалле Amber.

    000004a0 <_outbyte>:
    4a0: e59f1454 ldr r1, ; 8fc < адрес регистра данных UART >
    4a4: e59f3454 ldr r3, ; 900 < адрес регистра статуса UART >
    4a8: e5932000 ldr r2, ; прочитаем текущий статус
    4ac: e2022020 and r2, r2, #32
    4b0: e3520000 cmp r2, #0 ; проверяем, что UART не занят
    4b4: 05c10000 strbeq r0, ; записываем символ в UART только если он не занят
    4b8: 01b0f00e movseq pc, lr ; условный возврат из процедуры, если UART не был занят
    4bc: 1afffff9 bne 4a8 <_outbyte+0x8> ; цикл на проверку статуса UART

    Я думаю из комментариев этого фрагмента понятно, как работает эта процедура.

    Еще важное замечание по переходам. Регистр r15 (pc) может быть использован в обычных арифметических или логических операциях в качестве регистра приемника. Так что команда вроде add pc,pc,#8 вполне себе является инструкцией для перехода на другой адрес.

    По поводу переходов нужно сделать еще одно замечание. В более старших процессорах ARM есть еще дополнительные команды переходов bx , blx и blj . Это команды для переходов на фрагменты кода с другой системой команд. Bx /blx позволяет делать переход на 16-ти битный код THUMB процессоров ARM. Blj - это вызов процедур системы команд Jazelle (поддержка языка Java в ARM процессорах). В нашем Amber ARM v2a этих команд нет.

    В настоящее время для программирования даже достаточно простых микроконтроллеров используются языки высокого уровня, как правило, являющиеся подмножествами языка С или С++.

    Однако при изучении архитектуры процессоров и ее особенностей целесообразно использовать языки Ассемблера, так как только такой подход может обеспечить выявление особенностей изучаемой архитектуры. По этой причине дальнейшее изложение ведется с использованием языка Ассемблера.

    Прежде чем приступить к рассмотрению команд ARM7, необходимо отметить следующие ее особенности:

      Поддержку двух наборов команд: ARM с 32-битными командами и THUMB с 16-битными командами. Далее рассматривается 32-битный набор команд, слово ARM будет означать команды, принадлежащие к этому формату, а слово ARM7 - собственно ЦПУ.

      Поддержку двух форматов 32-х разрядного адреса: с обратным порядком бит (big-endian processor и с прямым порядком бит (little-endian processor)). В первом случае старший бит (Most Significant Bit - MSB) располагается в младшем бите слова, а во втором случае - в старшем. Это обеспечивает совместимость с другими семействами 32-х разрядных процессоров при использовании языков высокого уровня. Однако в ряде семейств процессоров с ядром ARMиспользуется только прямой порядок байтов (т.е. MSB является самым старшим битом адреса), что значительно облегчает работу с процессором. Поскольку компилятор, используемый для ARМ7, работает с код в обоих форматах, необходимо удостовериться, что формат слов задан правильно, в противном случае полученный код будет «вывернут наизнанку».

      Возможность выполнения различных типов сдвига одного из операндов «на проходе» перед использованием в АЛУ

      Поддержка условного выполнения любой команды

      Возможность запрета изменения флагов результатов выполнения операции.

        1. Условное выполнение команд

    Одна из важных особенностей набора команд ARM заключается в том, что поддерживается условное выполнение любой команды. В традиционных микроконтроллерах единственными условными командами являются команды условных переходов, и, быть может, ряд других, таких как команды проверки либо изменения состояния отдельных битов. В наборе команд ARM старшие 4 бита кода команды всегда сравниваются с флагами условий в регистре CPSR. Если их значения не совпадают, команда на стадии дешифрации заменяется команда NOP (нет операции).

    Это существенно сокращает время выполнения участков программы с «короткими» переходами. Так, например, при решении квадратных уравнений с вещественными коэффициентами и произвольными корнями при отрицательном дискриминанте необходимо перед вычислением квадратного корня сменить знак дискриминанта, а результат присвоить мнимой части ответа.

    При традиционном решении этой задачи необходимо ввести команду условного перехода. Выполнение этой команды занимает как минимум 2 такта – дешифрация и загрузка нового значения адреса в счетчик команд и дополнительное число тактов на загрузку конвейера команд. При использовании условного выполнения команды при положительном дискриминанте команда смены знака заменяется пустой операцией. При этом не происходит очистка конвейера команд, и потери составляют не более одного такта. Порог, при котором замена условных команд командой NOP оказывается эффективнее выполнения традиционных команд условного перехода и связанного с этим повторным заполнением конвейера равен его глубине, т.е. трем.

    Для реализации этой возможности к базовым мнемоническим обозначениям команд ассемблера (и С то же), нужно добавить любой из шестнадцати префиксов, определяющих тестируемые состояния флагов условий. Эти префиксы приведены в табл. 3. Соответственно существует 16 вариантов каждой команды. Например, следующая команда:

    MOVEQ R1, #0x008

    означает, что загрузка числа 0x00800000 в регистр R1 будет произведена только том случае, если результат выполнения последней команды обработки данных был «равно» или получен 0 результат и соответственно установлен флаг (Z) регистра CPSR.

    Таблица 3

    Префиксы команд

    Значение

    Z установлен

    Z сброшен

    С установлен

    Выше или равно (беззнаковое)

    C сброшен

    Ниже (беззнаковое)

    N установлен

    Отрицательный результат

    N сброшен

    Положительный результат или 0

    V установлен

    Переполнение

    V сброшен

    Нет переполнения

    С установлен,

    Z сброшен

    Выше (беззнаковое)

    С сброшен,

    Z установлен

    Ниже или равно (беззнаковое)

    Больше или равно (знаковое)

    N не равен V

    Меньше (знаковое)

    Z сброшен И

    (N равен V)

    Больше (знаковое)

    Z установлен ИЛИ

    (N не равен V)

    Меньше или равно (знаковое)

    (игнорируются)

    Безусловное выполнение

    Если вы используете дистрибутив Raspbian в качестве операционной системы вашего Raspberry Pi, вам понадобятся две утилиты, а именно, as (ассемблер, который преобразует исходный код на языке ассемблера в бинарный код) и ld (линковщик, который создает результирующий исполняемый файл). Обе утилиты находятся в пакете программного обеспечения binutils , поэтому они уже могут присутствовать в вашей системе. Разумеется, вам также понадобится хороший текстовый редактор; я всегда рекомендую использовать Vim для разработки программ, но он имеет высокий порог вхождения, поэтому Nano или любой другой текстовый редактор с графическим интерфейсом также отлично подойдет.

    Готовы начать? Скопируйте следующий код и сохраните его в файле myfirst.s:

    Global _start _start: mov r7, #4 mov r0, #1 ldr r1, =string mov r2, #stringlen swi 0 mov r7, #1 swi 0 .data string: .ascii "Ciao!\n" stringlen = . - string

    Эта программа всего-навсего выводит строку "Ciao!" на экран и если вы читали статьи, посвященные использованию языка ассемблера для работы с центральными процессорами архитектуры x86, некоторые из использованных инструкций могут быть вам знакомы. Но все же, существует множество различий между инструкциями архитектур x86 и ARM, что также можно сказать и синтаксисе исходного кода, поэтому мы подробно разберем его.

    Но перед этим следует упомянуть о том, что для ассемблирования приведенного кода и связывания результирующего объектного файла в исполняемый файл нужно использовать следующую команду:

    As -o myfirst.o myfirst.s && ld -o myfirst myfirst.o

    Теперь вы можете запустить созданную программу с помощью команды./myfirst . Вы наверняка обратили внимание на то, что исполняемый файл имеет очень скромный размер около 900 байт - если бы вы использовали язык программирования C и функцию puts() , размер бинарного файла был бы больше примерно в пять раз!

    Создание собственной операционной системы для Raspberry Pi

    Если вы читали предыдущие статьи серии, посвященные программированию на языке ассемблера для архитектуры x86, вы наверняка помните тот момент, когда вы в первый раз запустили свою собственную операционную систему, выводящую сообщение на экран без помощи Linux или какой-либо другой операционной системы. После этого мы доработали ее, добавив простой интерфейс командной строки и механизм загрузки и запуска программ с диска, оставив задел на будущее. Это была очень интересная, но не очень сложная работа главным образом благодаря помощи со стороны прошивки BIOS - она предоставляла упрощенный интерфейс для доступа к экрану, клавиатуре и устройству чтения флоппи-дисков.

    В случае Raspberry Pi в вашем распоряжении больше не будет полезных функций BIOS, поэтому вам придется самостоятельно разрабатывать драйверы для устройств, что само по себе является сложной и малоинтересной работой по сравнению с рисованием на экране и реализацией механизма исполнения собственных программ. При этом в сети существует несколько руководств, в которых подробно описаны начальные этапы процесса загрузки Raspberry Pi, особенности механизма доступа к выводам GPIO и так далее.

    Одним из лучших подобных документов является документ под названием Baking Pi (www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/index.html) от сотрудников Университета Кэмбриджа. По сути, он является набором руководств, описывающих приемы работы с языком ассемблера для включения светодиодов, доступа к пикселям на экране, получения клавиатурного ввода и так далее. В процессе чтения вы узнаете очень много об аппаратном обеспечении Raspberry Pi, причем руководства были написаны для оригинальных моделей этих одноплатных компьютеров, поэтому нет никаких гарантий того, что они будут актуальны для таких моделей, как A+, B+ и Pi 2.

    Если вы предпочитаете язык программирования C, вам следует обратиться к документу с ресурса Valvers, расположенному по адресу http://tinyurl.com/qa2s9bg и содержащему описание процесса настройки кросскомпилятора и сборки простейшего ядра операционной системы, причем в разделе Wiki полезного ресурса OSDev, расположенном по адресу http://wiki.osdev.org/Raspberry_Pi_Bare_Bones , также приведена информация о том, как создать и запустить простейшее ядро ОС на Raspberry Pi.

    Как говорилось выше, самой большой проблемой в данном случае является необходимость разработки драйверов для различных аппаратных устройств Raspberry Pi: контроллера USB, слота SD-карты и так далее. Ведь даже код для упомянутых устройств может занять десятки тысяч строк. Если вы все же хотите разработать собственную полнофункциональную операционную систему для Raspberry Pi, вам стоит посетить форумы по адресу www.osdev.org и поинтересоваться, не разработал ли уже кто-либо драйверы для этих устройств и, при наличии возможности, адаптировать их для ядра своей операционной системы, сэкономив тем самым большое количество своего времени.

    Как все это работает

    Первые две строки кода являются не инструкциями центрального процессора, а директивами ассемблера и линковщика. Каждая программа должна иметь четко заданную точку входа под названием _start , причем в нашем случае она оказалась в самом начале кода. Таким образом мы сообщаем линковщику, что исполнение кода должно начинаться с первой же инструкции и никаких дополнительных действий не требуется.

    С помощью следующей инструкции мы помещаем число 4 в регистр r7 . (Если вы никогда не работали с языком ассемблера ранее, вам следует знать, что регистром называется ячейка памяти, расположенная непосредственно в центральном процессоре. В большинстве современных центральных процессоров реализовано небольшое количество регистров по сравнению с миллионами или миллиардами ячеек оперативной памяти, но при этом регистры незаменимы, так как работают гораздо быстрее.) Чипы архитектуры ARM предоставляют разработчикам большое количество регистров общего назначения: разработчик может использовать до 16 регистров с именами от r0 до r15 , причем эти регистры не связаны с какими-либо историческими сложившимися ограничениями, как в случае архитектуры x86, где некоторые из регистров могут использоваться для определенных целей в определенные моменты.

    Итак, хотя инструкция mov и очень похожа на одноименную инструкцию архитектуры x86, вам в любом случае следует обратить внимание на символ решетки рядом с числом 4 , указывающий на то, что далее расположено целочисленное значение, а не адрес в памяти. В данном случае мы желаем использовать системный вызов write ядра Linux для вывода нашей строки; для использования системных вызовов следует заполнять регистры необходимыми значениями перед тем, как простить ядро выполнить свою работу. Номер системного вызова должен помещаться в регистр r7 , причем число 4 является номером системного вызова write.

    С помощью следующей инструкции mov мы помещаем дескриптор файла, в который должна быть записана строка "Ciao!", то есть, дескриптор стандартного потока вывода, в регистр r0 . Так как в данном случае используется поток стандартного вывода, в регистр помещается его стандартный дескриптор, то есть, 1 . Далее нам нужно поместить адрес строки, которую мы хотим вывести, в регистр r1 с помощью инструкции ldr (инструкция "загрузки в регистр"; обратите внимание на знак равенства, указывающий на то, что далее следует метка, а не адрес). В конце кода, а именно, в секции данных мы объявляем эту строку в форме последовательности символов ASCII. Для успешного использования системного вызова "write" нам также придется сообщить ядру операционной стемы о том, какова длина выводимой строки, поэтому мы помещаем значение stringlen в регистр r2 . (Значение stringlen рассчитывается путем вычитания адреса окончания строки из адреса ее начала.)

    На данный момент мы заполнили все регистры необходимыми данными и готовы к передаче управления ядру Linux. Для этого мы используем инструкцию swi , название которой расшифровывается как "software interrupt" ("программное прерывание"), осуществляющую переход в пространство ядра ОС (практически таким же образом, как и инструкция int в статьях, посвященных архитектуре x86). Ядро ОС исследует содержимое регистра r7 , обнаруживает в нем целочисленное значение 4 и делает вывод: "Так, вызывающая программа хочет вывести строку". После этого оно исследует содержимое других регистров, осуществляет вывод строки и возвращает управление нашей программе.

    Таким образом мы видим на экране строку "Ciao!", после чего нам остается лишь корректно завершить исполнение программы. Мы решаем эту задачу путем помещения номера системного вызова exit в регистр r7 с последующим вызовом инструкции программного прерывания номер ноль. И на этом все - ядро ОС завершает исполнение нашей программы и мы снова перемещаемся в командную оболочку.

    Vim (слева) является отличным текстовым редактором для написания кода на языке ассемблера - файл для подсветки синтаксиса данного языка для архитектуры ARM доступен по ссылке http://tinyurl.com/psdvjen .

    Совет: при работе с языком ассемблера следует не скупиться на комментарии. Мы не использовали большого количества комментариев в данной статье для того, чтобы код занимал как можно меньше места на страницах журнала (а также потому, что мы подробно описали назначение каждой из инструкций). Но при разработке сложных программ, код которых кажется очевидным на первый взгляд вы всегда должны задумываться о том, как он будет выглядеть после того, как вы частично забудете синтаксис языка ассемблера для архитектуры ARM и вернетесь к разработке по прошествии нескольких месяцев. Вы можете забыть обо всех использованных в коде трюках и сокращениях, после чего код будет выглядеть как полнейшая абракадабра. Исходя из всего вышесказанного, следует добавлять в код как можно больше комментариев, даже в том случае, если некоторые из них кажутся слишком очевидными в текущий момент!

    Обратный инжиниринг

    Преобразование бинарного файла в код на языке ассемблера также может оказаться полезным в некоторых случаях. Результатом данной операции обычно является не особо качественно оформленный код без читаемых имен меток и комментариев, который тем не менее, может пригодиться для изучения преобразований, которые были выполнены ассемблером с вашим кодом. Для дизассемблирования бинарного файла myfirst достаточно выполнить следующую команду:

    Objdump -d myfirst

    Эта команда позволит осуществить дизассемблирование секции исполняемого кода бинарного файла (но не секции данных, так как она содержит текст в кодировке ASCII). Если вы ознакомитесь с кодом, полученным в результате дизассемблирования, вы наверняка заметите, что инструкции в нем практически не отличаются от инструкций в оригинальном коде. Дизассемблеры используются главным образом тогда, когда нужно изучить поведение программы, которая доступна лишь в форме бинарного кода, например, вируса или простой программы с закрытым исходным кодом, поведение которой вы желаете эмулировать. При этом вы должны всегда помнить об ограничениях, накладываемых автором исследуемой программы! Дизассемблирование бинарного файла программы и простое копирование полученного кода в код вашего проекта, разумеется, является плохой идеей; при этом вы вполне можете использовать полученный код для изучения принципа работы программы.

    Подпрограммы, циклы и условные инструкции

    Теперь, когда мы знаем, как разрабатывать, ассемблировать и связывать простые программы, давайте перейдем к рассмотрению кое-чего более сложного. В следующей программе для вывода строк используются подпрограммы (благодаря им мы можем повторно использовать фрагменты кода и избавить себя от необходимости выполнения однотипных операций заполнения регистров данными). В данной программе реализован главный цикл обработки событий, который позволяет осуществлять вывод строки до того момента, как пользователь введет "q". Изучите код и попытайтесь понять (или угадать!) назначение инструкций, но не отчаивайтесь, если вам что-то не понятно, ведь чуть позже мы также рассмотрим его в мельчайших подробностях. Обратите внимание на то, что с помощью символов @ в языке ассемблера для архитектуры ARM выделяются комментарии.

    Global _start _start: ldr r1, =string1 mov r2, #string1len bl print_string loop: mov r7, #3 @ read mov r0, #0 @ stdin ldr r1, =char mov r2, #2 @ два символа swi 0 ldr r1, =char ldrb r2, cmp r2, #113 @ Код ASCII символа "q" beq done ldr r1, =string2 mov r2, #string2len bl print_string b loop done: mov r7, #1 swi 0 print_string: mov r7, #4 mov r0, #1 swi 0 bx lr .data string1: .ascii "Enter q to quit!\n" string1len = . - string1 string2: .ascii "That wasn"t q...\n" string2len = . - string2 char: .word 0

    Наша программа начинается с помещения указателя на начало строки и значения ее длины в соответствующие регистры для последующего осуществления системного вызова write , причем сразу же после этого осуществляется переход к подпрограмме print_string , расположенной ниже в коде. Для осуществления этого перехода используется инструкция bl , название которой расшифровывается как "branch and link" ("ветвление с сохранением адреса"), причем сама она сохраняет текущий адрес в коде, что позволяет вернуться к нему впоследствии с помощью инструкции bx . Подпрограмма print_string просто заполняет другие регистры для осуществления системного вызова write таким же образом, как и в нашей первой программе перед переходом в пространство ядра ОС с последующим возвратом к сохраненному адресу кода с помощью инструкции bx .

    Вернувшись к осуществляющему вызов коду, мы можем обнаружить метку под названием loop - название метки уже намекает на то, что мы вернемся к ней через некоторое время. Но сначала мы используем еще один системный вызов с именем read (под номером 3) для чтения символа, введенного пользователем с помощью клавиатуры. Поэтому мы помещаем значение 3 в регистр r7 и значение 0 (дескриптор стандартного потока ввода) в регистр r0 , так как нам нужно прочитать пользовательский ввод, а не данные из файла.

    Далее мы размещаем адрес, по которому мы хотим сохранить символ, прочитанный и помещенный ядром ОС в регистр r1 - в нашем случае это область памяти char , описанная в конце секции данных. (На самом деле, нам нужно машинное слово, то есть, область памяти для хранения двух символов, ведь в ней будет храниться и код клавиши Enter. При работе с языком ассемблера важно всегда помнить о возможности переполнения областей памяти, ведь в нем нет никаких высокоуровневых механизмов, готовых прийти вам на помощь!).

    Вернувшись к основному коду, мы увидим, что в регистр r2 помещается значение 2 , соответствующее двум символам, которые мы хотим сохранить, после чего осуществляется переход в пространство ядра ОС для выполнения операции чтения. Пользователь вводит символ и нажимает клавишу Enter. Теперь нам нужно проверить, что это за символ: мы помещаем адрес области памяти (char в секции данных) в регистр r1 , после чего с помощью инструкции ldrb загружаем байт из области памяти, на которую указывает значение из этого регистра.

    Квадратные скобки в данном случае указывают на то, что данные хранятся в интересующей нас области памяти, а не в самом регистре. Таким образом, регистр r2 теперь содержит единственный символ из области памяти char из секции данных, причем это именно тот символ, который ввел пользователь. Наша следующая задача будет заключаться в сравнении содержимого регистра r2 с символом "q" , который является 113 символом таблицы ASCII (обратитесь к таблице символов, расположенной по адресу www.asciichart.com). Теперь мы используем инструкцию cmp для выполнения операции сравнения, после чего используем инструкцию beq , имя которой расшифровывается как "branch if equal" (переход при условии равенства), для перехода к метке done в том случае, если значение из регистра r2 равно 113. Если это не так, то мы выводим нашу вторую строку, после чего осуществляем переход к началу цикла с помощью инструкции b .

    Наконец, после метки done мы сообщаем ядру ОС о том, что мы хотим завершить исполнение программы, точно так же, как и в первой программе. Для запуска данной программы следует просто осуществить ее ассемблирование и связывание в соответствии с инструкциями, приведенными для первой программы.

    Итак, мы рассмотрели достаточно большой объем информации в максимально сжатой форме, но будет лучше, если вы займетесь самостоятельным изучением материала, экспериментируя с приведенным выше кодом. Нет лучшего способа знакомства с языком программирования, чем проведение экспериментов, заключающихся в модификации чужого кода и наблюдении за достигнутым эффектом. Теперь вы можете разрабатывать простые программы на языке ассемблера для архитектуры ARM, осуществляющие чтение пользовательского ввода и вывод данных, при этом использующие циклы, операции сравнения и подпрограммы. Если вы не сталкивались с языком ассемблера до сегодняшнего дня, я надеюсь, что данная статья сделала этот язык немного более понятным для вас помогла развеять популярный стереотип о том, что он является мистическим ремеслом, доступным лишь нескольким талантливым разработчикам.

    Разумеется, приведенная в статье информация относительно использования языка ассемблера для архитектуры ARM является всего лишь вершиной айсберга. Использование данного языка программирования всегда связано с огромным количеством нюансов и если вы хотите, чтобы мы написали о них в одной из следующих статей, просто дайте нам знать об этом! Пока же рекомендуем посетить отличный ресурс с множеством материалов для изучения приемов создания программ для систем Linux, исполняющихся на компьютерах с центральными процессорами архитектуры ARM, который расположен по адресу http://tinyurl.com/nsgzq89 . Удачного программирования!

    Предыдущие статьи из серии "Школа ассемблера":



    Понравилась статья? Поделиться с друзьями: