Базовые макросы. Взгляд из ассемблера ARM Cortex-M.

Всем привет.

Здесь я коротко напишу про вид из ассемблера базовых макросов min(a,b), max(a,b) и isEqual(a,b).

Первые два иногда пригождаются, третий — весьма спорный, но иногда его удобно использовать.

Заметка будет короткой и наверняка не интересной для профи, которые знакомы с архитектурой ARM-Cortex, и понимающие отличия ARM-режима процессора от THUMB-режима) Кому все же интересно — добро пожаловать под кат.

we.zloy_pakimon.arm_asm_macro_1


Изучаю STM32 на примере завалявшейся платки STM32VLDiscovery, бывшей когда-то, помнится, весьма популярной. Стало интересно во что компилируются самые простые макросы, и насколько накладно их использование.
Использую GCC (Code Sourcery) + Eclipse + OpenOCD/Zylin на Mac OS X.

Итак, сами макросы:

/**
 * Min() macros
 * @input a,b
 * @return min (a,b)
 **/
#define min(a,b) ((a) < (b) ? (a) : (b))

/**
 * Max() macros
 * @input a,b
 * @return max (a,b)
 **/
#define max(a,b) ((a) > (b) ? (a) : (b))

/**
 * isEqual() macros
 * @input a,b
 * @return TRUE (1) if a == b, else return FALSE (0)
 **/
#define isEqual(a,b) ((a) == (b) ? (1) : (0))


Код, использующий эти макросы:

  uint8_t a = 3;
  uint8_t b = 2;
  uint8_t c = max(a,b);
  uint8_t d = min(a,b);
  if (isEqual(c,d)) {
	  register uint32_t register_variable = c;
	  register_variable++;
	  uint32_t local_variable = register_variable;
  }
  else {
	  register uint32_t register_variable = d;
	  register_variable--;
	  uint32_t local_variable = register_variable;
  }


Всё довольно бессмысленно и беспощадно, но суть не в этом.
Разберём, во что разворачивается этот код.

Инициализация переменных.

Чтобы сразу понять как инициализируются переменные, нужно прокрутить asm-листинг чуть выше и посмотреть на строчки, с которых начинается функция main():

08000cbc:   push {r4, r7, lr}
08000cbe:   sub sp, #36     ; 0x24
08000cc0:   add r7, sp, #0

1. Сначала мы сохраняем в стек регистры r4, r7 и lr(r14, в котором хранится вектор возврата при вызове функций).
2. Далее вычитаем из sp (r13 — указатель вершины стека) число 36 (необходимое для последующего сохранения в стек 36 байт).
3. И, наконец, сохраняем новое значение sp в r7 командой:

add r7, sp, #0

Которая в данном случае эквиваентна команде:

mov r7, sp


Далее, собственно, код инициализации:

08000d10:   mov.w r3, #3
08000d14:   strb r3, [r7, #27]
08000d16:   mov.w r3, #2
08000d1a:   strb r3, [r7, #26]

Все переменные в нашем примере локальные, а значит живут на стеке (кроме переменной с модиикатором register, но о ней позже). Переменные a и b занимают всего лишь по 1 байту, и a живёт по адресу в стеке sp + 27d, а b по адресу sp + 26d. Команда mov.w — это 32-битная (wide) команда mov в Thumb-2. Суффикс -b в команде strb говорит о том, что мы хотим сохранить из регистра в память unsigned byte переменную.

Max(a,b)
Код, в который разворачивается строка:

uint8_t c = max(a,b);


… тоже незамысловат:

08000d1c:   ldrb r2, [r7, #26]
08000d1e:   ldrb r3, [r7, #27]
08000d20:   cmp r2, r3
08000d22:   it cs
08000d24:   movcs r3, r2
08000d26:   strb r3, [r7, #25]

1. Сначала мы загружаем из стека в r2 значение по адресу sp + 26d (как мы помним, это переменная b).
2. А в регистр r3 значение переменной a.
3. Далее сравниваем эти два значения командой cmp. Эта команда работает точно так же как вычитание — sub, только результат нигде не сохраняется. В данном случае из r2 (= b = 2) вычитается r3 (= a = 3).
4. И далее весёлая команда, которая есть в наборе инструкций THUMB/THUMB-2, но её нет в наборе инструкций ARM.
it — это команда if-then, после которой могут следовать блоки из 4 команд, для условия инструкции it, или инверсного условия. То есть мы можем объявить условие выполнения/невыполнения для нескольких последующих инструкций после it. В принципе, эту команду можно опустить, но она гарантирует, что после неё будут команды, отрабатывающие объявленное условие.
Итак, it cs — это объявление условия CS — unsigned higher or same. Так как мы сравнивали r2(b) с r3(a), то если r2 > r3, то должно выполниться условие CS, и, соответственно должны выполниться команды с условиями cs, следующие после it.
5. Следующая команда movcs r3, r2mov с условием cs. То есть, если r2 > r3 — она выполнится, иначе — не выполнится. В нашем случае она не выполняется, что легко заметить в отладчике.
Регистры до выполнения movcs:
we.zloy_pakimon.arm_asm_macro_2

Регистры после выполнения movcs:
we.zloy_pakimon.arm_asm_macro_3

6. И завершающая команда strb r3, [r7, #25] — сохраняем результат в переменную c, лежащую в стеке по адресу sp + 25d. Эта команда уже не относится к макросу, а относится к инициализации переменной c.

Как видим, всё очень просто.

Min(a,b)
Код этого макроса (с такой же инициализацией в конце) почти ничем не отличается от предыдущего:

ldrb r2, [r7, #26]
08000d28:   ldrb r2, [r7, #26]
08000d2a:   ldrb r3, [r7, #27]
08000d2c:   cmp r2, r3
08000d2e:   it cc
08000d30:   movcc r3, r2
08000d32:   strb r3, [r7, #24]

Как видим, в коде макроса вообще всего 1 отличие — это условие CC (unsigned lower), согласно которому в нашем случае movcc не будет игнорироваться, и в r3 (= a = 3) окажется значение r2 (= b = 2), которым и будет проинициализирована переменная d по адресу sp + 24.d.

isEqual(a,b)
В случае с isEqual удобно посмотреть на asm-листинг так, как он коррелирует с c-листингом:

 93               if (isEqual(c,d)) {
08000d34:   ldrb r2, [r7, #25]
08000d36:   ldrb r3, [r7, #24]
08000d38:   cmp r2, r3
08000d3a:   bne.n 0x8000d46 <main+138>
 94       	  register uint32_t register_variable = c;
08000d3c:   ldrb r4, [r7, #25]
 95       	  register_variable++;
08000d3e:   add.w r4, r4, #1
 96       	  uint32_t local_variable = register_variable;
08000d42:   str r4, [r7, #20]
08000d44:   b.n 0x8000d4e <main+146>
 99       	  register uint32_t register_variable = d;
08000d46:   ldrb r4, [r7, #24]
 100       	  register_variable--;
08000d48:   add.w r4, r4, #4294967295       ; 0xffffffff
 101       	  uint32_t local_variable = register_variable;
08000d4c:   str r4, [r7, #16]


Здесь тоже нет ничего сложного, только я хотел посмотреть как себя ведут в программе переменные с модификатором register. При оптимизации -o0, любая, даже не используемая локальная переменная сохраняется на стеке — то есть ей выделяется память. Если инициализировать (объявлять) переменные с модификатором register, то, при их не использовании, даже с -o0 компилятор просто выбрасывает код, связанный с их инициализацией. Это я на всякий случай посмотрел из ассемблера, а здесь оставил только для того, чтобы показать, как выглядят эти переменные в asm-листинге.

Итак, начнём:
1, 2. Загружаем из стека c и d в r2 и r3 соответственно.
3. Сравниваем r2 и r3.
4. Если r2 и r3 равны, то после выполнения предыдущей инструкции, поднимается флаг Z. bne.n — выполняется, если флаг Z НЕ поднялся, иначе не выполняется. Суффикс .n указывает на то, что это 16-битная инструкция из набора команда THUMB/THUMB-2. То есть, если r2 == r3, то флаг Z поднимается и инструкция bne.n игнорируется, если же r2 != r3 (как в нашем случае), то Z НЕ поднимаается и мы шагаем на адрес 0x8000d46 обрабатывать блок else.
5. Продолжим с адреса 0x8000d46 (вариант с выполнением условия блока if примерно такой же). Загружаем в r4 переменную d из стека по адресу sp + 24. Обратите внимание, хоть мы и объявили переменную uint32_t register_variable, но компилятор, исходя из того, что она объявлена с модификатором register, не выделяет под неё память в стеке (хотя, если вспомнить с чего начинается функция main(), то r4 сохраняется в стек в самом начале).
5. Ну а далее, думаю, всё совсем очевидно. add.w r4, r4, #4294967295 — добавляем к r4 значение 0xFFFFFFFF, что эквивалентно sub.w r4, #1.
6. Сохраняем r4 в переменную local_variable по адресу sp + 16. Она займёт там 4 байта, так как её тип — uint32_t.

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

На этом, думаю, мы и закончим)
Если тема интересная, то пишите в комментариях, я люблю порыться в asm-коде программ, чтобы понять как там что работает, и могу попробовать написать подобные заметки и про другие макросы/функции.
Если вам не понравилось, или вы нашли ошибки — тоже пишите, буду исправляться)

Спасибо за внимание.

Stay heavy \m/

Комментарии (14)

RSS свернуть / развернуть
На -o0 это не интересно. Интересней было бы посмотреть на оптимизированный код — но код в примере будет просто выкинуть компилятором целиком :)

А заче команда it, если команды после нее все равно идут условные? Я бы понял, если бы условность команд выкинули,
0
  • avatar
  • Vga
  • 17 октября 2014, 15:26
Упс. Улетело раньше времени. Как раз когда хотел стереть незаконченное предложение.
0
На infocenter.arm.com написано, что 16-битные Thumb-команды, которые обычно изменяют флаги, внутри блога it перестают изменять флаги. Но в данном случае, насколько я могу понять, команда it избыточна.
Попробую посмотреть исчезнет ли она с оптимизацией)
0
Нет, видимо не исчезнет.
В мануалах написано, что Arm-инструкции содержат 4 бита, для имплементации условности, в Thumb места для этих данных нет, по-этому условные команды работают в связке с IT.
С другой стороны, в Thumb-2 есть и 32-битные команды, могут ли они условно работать без IT пока не понятно.
0
Эти условия зашифрованы в команде IT, но в ассемблере они отображены на команды (если посмотреть коды команд, то они, вроде, ничем от обычных отличаться не должны). Без IT условное исполнение имеют, вроде, лишь переходы в наборе команд Thumb/Thumb2. По сути эта команда добавляет условное поле к нескольким последующим инструкциям =) К одной, двум, трём. Но только либо прямое (then), либо только обратное (else).
0
Т.е. в отличие от ARM, в Thumb-2 инструкция MOVCC ничем не отличается от MOV?
Но вообще, возникает вопрос — чем эта инструкция отличается от сверхкороткого джампа? В ARM идея быа в том, чтобы не использовать дополительную команду прыжка, если нужно выполнять по условию всего одну-две команды.
0
Да, так как в опкоде нет полей условия.
Так всё затем же, что и в арм, — чтоб не использовать прыжок (за него же штрафы есть к конвейеру?). Просто условное поле даётся предварительно отдельной командой (баф прямо), в отличие от арма, где оно неотделимо от команд вовсе.

Есть ещё ограничения какие-то, связанные с этой инструкцией: то ли из зоны действия условных команд прыгать нельзя и вызывать подпрограммы, то ли что-то подобное.
0
Забавное решение. А отключенные этим условием команды обрабатываются как нопы, как в ARM7?
0
Видимо так.
0
Прыжок должен быть последней инструкцией в блоке, иначе неопределённое поведение программы.
Команды сравнения внутри блока не имеют влияния на флаги.
0
Таким образом, можно увидеть, что простые макросы особо не нагружают ни процессор, ни память, и их использовать можно везде, где удобно и семантично)
Такое утверждение не совсем корректно, т. к. макросы – это просто синтаксическая конструкция, которая обрабатывается препроцессором. А потом уже полученный после подстановки код компилируется. По факту
uint8_t c = max(a,b);

препроцессор развернет в
uint8_t c = a > b ? a : b;

и уже этот код будет во что-то скомпилирован. Во что – зависит от многих факторов. Например, от типа переменных (для переменных с плавающей точкой результат будет далеко не так тривиален как получилось у Вас в примере), от оптимизатора, от самого компилятора (разные компиляторы могут генерировать разный код).
+3
  • avatar
  • e_mc2
  • 17 октября 2014, 22:33
Согласен.
Примерно это я имел ввиду, только не совсем корректно выразился)
О компиляторе я писал в самом начале — это gcc codesourcery g++, разные уровни оптимизации — да, не смотрел (гляну на днях, интересно).
С float тоже чето не дошло попробовать. Вроде есть инструкция vcmp, но я не уверен, что она поддерживается stm32f1xx.
0
А макросы то опасные, никто и не сказал. То есть вот такой код может сделать не то, что от него ожидали.

a = min(foo(), bar());

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