Вместо введения. Чем ARM процессоры необычны. (Часть 1)
Младшие версии ARM процессоров имеют 32 разрядный набор команд, ориентированный преимущественно на обработку 32-х разрядных данных.
Высокая разрядность данных, заложенная в ARM изначально, хорошо подходит для устройств с большой вычислительной нагрузкой. "Широкие" команды дают возможность в коде одной команды реализовать достаточно сложную обработку данных, для которой в других процессорах потребуется несколько команд - например, совместить сдвиг, сумму, и перенос в другой регистр. Но с другой стороны, эти же свойства снижают применимость младших версий ARM процессоров в качестве вычислительных ядер для недорогих, встраиваемых или мобильных устройств с малоразрядной периферией, получивших сейчас большое распространение. Для реализации простой логики управления 4 байтная команда занимает слишком много места в памяти, а 32 разряда данных используются неэффективно.
Для придания современным версиям ARM процессоров большей универсальности, им добавили второй набор, уже 2 байтных команд, гораздо более эффективный по размеру кода. Набор укороченных команд называется Thumb, а набор 'стандартных' команд- ARM наборами. Несмотря на то, что 'внутри' процессора команды Thumb преобразуются перед исполнением в ARM инструкции, с точки зрения программиста эти наборы настолько отличаются, что вполне могли бы принадлежать разным процессорам. Переход от одного набора к другому может выполняться в любом месте, практически любой алгоритм можно написать либо на ARM либо на Thumb, по усмотрению программиста. Но, поскольку в Thumb наборе нет команд, требуемых для обработки исключительных ситуаций (к примеру, сброса или прерываний), любая программа требует включения фрагментов ARM. После сброса или наступления исключительной ситуации процессор принудительно переключается к исполнению команд ARM набора.
Набор Thumb, имея меньшую разрядность, оптимизирован, в его состав ввели только команды, реализующие наиболее важные операции, и он несколько ограничивает доступ к ресурсам процессора. Например, с 16 до 8 меняется число полностью доступных регистров. Некоторые команды из набора Thumb предоставляют ограниченный доступ к регистрам R8..R15, называемых в этом режиме 'верхними', или 'старшими регистрами' (high registers). Указатель стека SP (R13), регистр адреса возврата LR (R14), счетчик команд PC (R15) доступны в Thumb через специальные команды, прямой доступ к словам состояния CPRS и SPRS не предоставляется.
Программная смена набора команд происходит при выполнении двух специальных команд - "BX" 'перехода на адрес с изменением' (Branch and eXchange) и вызова подпрограммы "BLX", (Branch, Link and eXchange). Если при исполнении этих команд младший разряд адреса перехода равен 1, процессор переключится в Thumb режим, если равен 0, то в ARM режим. Последующие команды, на которые осуществляется переход, будут рассматриваться как принадлежащие выбранному набору. Эти две команды присутствуют и в Thumb и в ARM, конечно для каждого набора свои версии.
Команды ARM режима длиной 4 байта, хранятся по адресам, кратным 4-м (0, 4, 8, 0xC и т.д.), а двухбайтные команды THUMB режима по адресам, кратным 2 (0, 2, 4...). Нетрудно заметить, что в обоих случаях последний разряд адреса равен нулю. Поэтому для реализации перехода в Thumb режим необходимо устанавливать этот бит 'принудительно', для чего к адресу прибавляют единицу.
Пример:
LDR R1,=TProc1 +1; ( * см. далее “Команды, которых нет”)
BX R1; переход на адрес TProc1 с изменением набора команд на Thumb.
В сам регистр R15 адрес загружается в любом случае с обнуленным младшим разрядом.
В отличии от команд 'BX' и 'BLX', применение 'обычных' команд 'B' и 'BL' или изменение значения регистра R15 любой командой, к смене набора команд не приводит. Поэтому в Thumb режиме +1 к каждому адресу перехода приписывать не надо, что значительно облегчает жизнь программиста.
Есть еще один способ сменить набор инструкций. Необходимо установить в нужное состояние флаг T в регистре SPSR и выполнить команду восстановления регистра CPSR из SPSR.
Замечание: в нормальном режиме исполнения программ регистр SPSR недоступен, он есть только в некоторых привилегированных режимах. Непосредственное изменение T флага в регистре CPSR приводит к НЕПРЕДСКАЗУЕМОМУ результату.
Команды, которых нет.
Еще одна особенность программирования для ARM, первое время затрудняющая работу - это наличие в исходных текстах программ или листингах деассемблера таких команд или способов адресации, которые не умеет исполнять процессор и которых, соответственно, нет в его описании. Это вызвано тем, что огромное разнообразие возможных форм команд вынуждает для самых часто используемых из них применять специальные сокращенные обозначения.
К примеру, формы автоматической адресации команды LDR вызывают немало вопросов.
Команда LDR Load Register служит для загрузки в указанный первым операндом регистр 32-х разрядного слова из памяти. Адрес памяти, откуда происходит загрузка, может вычисляться процессором 9-ю различными, порой довольно сложными способами. Подробно способы вычисления адреса будут рассмотрены далее, но сейчас отметим, что все они требуют, чтобы в качестве базового при вычислении адреса использовалось значение, хранящееся в одном из регистров R0:R15.
Самая простая форма вычисления адреса памяти для команды LDR такова:
LDR Rd, [Rm,#смещение]
- загрузить в Rd значение хранящееся по адресу Rm + смещение.
При нулевом смещении допустима такая упрощенная запись:
LDR Rd, [Rm] ; - синоним LDR Rd, [Rm, #0]
Получается, что если мы хотим при помощи самой простой формы команды LDR загрузить в регистр R1 число 0x22334455, нам нужно:
1. Сохранить это число в памяти (при компиляции, например)
2. Загрузить в любой регистр, скажем, в R2 адрес этого числа
3. Выполнить LDR R1,[R2]
А как нам выполнить пункт 2? Как нам загрузить в регистр 32 битный адрес? Выходом служит вычисление адреса данных относительно состояния регистра R15 (счетчик команд, PC), значение которого всегда определенно и на текущий момент известно – оно равно адресу выполняемой команды. Надо только учитывать, что при любом чтении из R15 в режиме ARM возвращается значение равное (адресу_текущей_команды + 8)*.
thisAddr: LDR R1,[R15, #SourceData - thisAddr - 8] ; коррекция на 8
...
SourceData: DD 0x22334455 ; недалеко в памяти данные
; это работающий пример загрузки в регистр R1 числа 0x22334455
Конечно, это сложно и неудобно. Для упрощения жизни программистов у ARM ассемблера есть специальные формы автоматической адресации (здесь под ассемблером подразумевается программа, переводящая исходный *.asm текст в объектный код):
-------------------------------------------------------------------------------
* Примечание: в Thumb чтение R15 дает значение, равное (адресу_текущей_команды + 4)
1. Для команд обмена регистра с памятью допустимо вторым операндом указывать непосредственно адрес, к содержимому которого необходимо обратиться. Выбор R15 в качестве базового регистра и вычисление смещения будут выполнены ассемблером автоматически, гарантируя правильную адресацию.
LDR R1, SourceData; прочитать значение по адресу SourceData в регистр
STR R1, SourceData; записать из регистра в адрес SourceData
SourceData: DD 0x22334455; адрес недалеко в памяти
2. Для загрузки в регистр конкретного значения можно указать вторым операндом не адрес хранения числа, а непосредственно требуемое число и добавить перед ним знак '='. Тогда все необходимые действия, вплоть до размещения числа в памяти, будут выполнены ассемблером автоматически, гарантируя загрузку требуемого значения.
LDR R1,=0x22334455 ; здесь R1 := 0x22334455
Или, как вариант загрузки произвольного 32 разрядного адреса:
...
Label_1:
LDR R1, =Label_1; здесь R1 := адресу Label_1 (адрес тоже Число)
...
Применяя эти формы команды, надо всегда помнить о том, что мы используем не свойство процессора, адресовать данные таким образом, а свойство программы - ассемблера разворачивать эти упрощенные вызовы в конструкции вида
LDR Rd, [PC, #offsetDataStore];
...
DataStore: DD 123456789;
Это накладывает ограничения на использование автоматической адресации. Во первых, смещение 12 разрядное (для команд LDRD STRD LDRH STRH LDRSH LDRSB даже 8 разрядное), то есть для загружаемых данных должно найтись место в пределах +/- 4095 (+/- 255) адресов от команды. Во вторых, понятно, что при сохранении данных командой STR надо убедится, что мы не пытаемся записывать в ПЗУ, или, при отладке, мы не должны писать в сегмент кода (запись в него запрещена). Но если в KEIL создать для имитации ОЗУ сегмент данных с разрешением записи, попытка автоматической адресации данных за пределы сегмента кода дает ошибку при ассемблировании:
error A20: INVALID SIMPLE RELOCATABLE EXPRESSION.
В некоторых случаях надо быть готовым к тому, что ассемблер заменит конструкцию LDR Rn,=data на команду MOV Rn, #data. Это произойдет, если загружаемое значение можно представить как любое 8 разрядное число от 0x00 до 0xff, циклически сдвинутое на произвольное четное количество разрядов (0,2,4...30) внутри 32 разрядного регистра (просто, не правда ли? =), в этом случае будет произведена оптимизация, и вместо ожидаемой команды LDR будет вставлена команда
MOV Rn,#data
где #data - так называемые непосредственные данные. Подробнее о них - дальше, в разделе addressing_mode.
IDA умеет 'сворачивать' команды LDR обратно в формы автоматической адресации, но тем не менее не прячет данные, которые участвуют в такой операции:
LDR PC, =loc_A000009C; свернутая форма
; -------------------------------------------------
off_A0000004 DCD loc_A000009C ; это те данные, которые грузятся
Еще две 'команды', которые встречаются в программах, но которые не умеет исполнять процессор:
ADR Rx, Address ; - загрузить в Rx указанный относительный адрес. Реально это команда превращается ассемблером в команду ADD Rx,PC,#xx или SUB Rx,PC,#xx, вычисляя нужное значение прибавлением или вычитанием смещения от PC. Очевидное ограничение - применимое для написания программ смещение между адресом команды и загружаемым адресом не более +/-255 байт.
RET – В действительности это команда MOV R15,R14.
Еще один вариант “автоматической” команды – MOVL Rx,#data
В Thumb режиме максимальное число, загружаемое в регистр командой mov Rx,#data равно 255. При необходимости загрузить большее число, применяют команду movl Rx,#data, которая автоматически будет заменена на пару команд:
mov Rx,#data
lsl Rx,Rx,#shift
или
mov Rx,#data1
add Rx,#data2
в зависимости от того, каким образом можно представить число, которое необходимо загрузить. В отличие от IDA, Keil такую форму команды не понимает