Аватар Night_Ghost

Автор:


2484

Arduino - кровь, кишки, ассемблер

24 декабря 2015, 13:08 | Мы Автор: Night_Ghost

Довелось мне тут поковырять одну поделку на AVR (коий известен по большей части тем что стоит в Ардуинах) дизассемблером. И мне настолько не понравилось обнаруженное, что я решил об этом высказаться, в основном дабы пар стравить.  Итак, самое ужасное:

0. Лучший дизассемблер "всех времен и народов" IDA Pro не умеет работать с кодом AVR. Нет, поддержка процессоров этого семейства в нем конечно же есть и команды он показывает, вопрос в том КАК.

во-первых, у АВР команда - 2 байта, поэтому адресация памяти программы для команд ICALL/IJMP (которые меняют указатель команд) не совпадает с адресацией команды LPM (которая выбирает байт из памяти программ), а дизассемблер этого
не учитывает. Поэтому обращения к константам в памяти программ (а их большинство!) приходится отслеживать исключительно вручную. Решением было бы вести виртуальный счетчик команд в байтах, а в командах перехода учитывать разницу адресации - но увы :(

Во-вторых, процессор 8-битный, а для адресации даже его куцей памяти приходится использовать 16-битные адреса - а значит загрузка любого адреса в регистры распадается на 2 команды - загрузку старшей и младшей частей. Поэтому дизассемблер ВООБЩЕ не знает ничего про адреса памяти, загружаемые как адреса! 

И даже в ручном режиме он никак не помогает в этом вопросе - ну нет у него  возможности указать что операнд команды - это старший/младший байт адреса памяти! Решением тут было бы объединение двух команд в макрос загрузки адреса в регистровую пару, но опять же увы...

Боль от двух предыдущих пунктов можно было бы слегка унять, если бы была возможность в комментарии сделать обращение к адресу в нужном сегменте, дабы оно вело себя как адресный операнд - но такой возможности тоже не предусмотрено!

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

Но это было лишь вступление о нелегком труде реверс-инженера, а дальше будет самая мякотка :)

1. Массивы.

Процессор, как уже было упомянуто, 8-битный, и хотя в нем есть команды 16-битной арифметики, но они ограничены значением +-31 байт, а остальные вычисления делаются побайтно. Пример - обращение к массиву (в памяти программы) по индексу:

mov r23, r22;                         тут у нас байтовый индекс

ldi r25, 0 ;                                беззнаковое расширение до слова

movw r30, r24;                     LPM работает с регистровой парой R30 R31

subi r30, -0xD4; ','               это так хитро прибавляется 0x5d4 - байтовый адрес массива в памяти программ

sbci r31, -6 ; '·'

lpm r22, Z;                              загрузили из памяти первый байт

movw r30, r24;                    снова индекс

subi r30, 0x18 ;                     снова массив - можно было бы оптимизировать, просто добавив разницу

sbci r31, -6 ; '·'

lpm r30, Z;                               наконец-то загрузили второй байт

Из этого куска кода видно, как же тяжело дается процессору адресная
арифметика.

Мораль такова: максимально избегать массивов в пользу адресных указателей, тогда при движении указателя будет просто добавляться небольшое число к регистровой паре.

 

2. Классы.

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

Маленький кусочек

cbi(*_ucsrb, UDRIE0);

который вроде как просто должен превратиться в команду CBI, на самом деле превращается в такую портянку

ldd r0, Z+18 ; 0x12

ldd r31, Z+19 ; 0x13

mov r30, r0

ld r24, Z

andi r24, 0xDF ; 223

st Z, r24

Вместо одной команды - шесть! Не, я конечно понимаю что универсальность это хорошо - только вот подавляющее большинство применений приходится на мелкие контроллеры, содержащий всего лишь один последовательный порт! Им эта универсальность не нужна, а расплачиваться приходится напрасным расходом и так крохотной памяти. (Что и послужило причиной написания собственной библиотеки SingleSerial для подобных применений)

Но если существование класса для последовательного порта еще можно понять - их все-таки бывает несколько, то с EEPROM все еще хуже. Зачем, кроме как ради красивости? Ну не может быть в архитектуре несколько разных EEPROM'ов :) К тому же для основных методов этого класса eeprom.read и eeprom.write вообще не нужны никакие данные класса - они просто вызывают eeprom_read_byte и eeprom_write_byte соответственно, и вполне могли бы быть статическими. А сейчас каждое обращение к EEPROM сопровождается двумя лишними командами - загрузкой адреса данных класса... Конечно, имеющийся класс позволяет мимикрировать работу с EEPROM как будто с обычными переменными - но в этой абстракции есть огромная дыра в виде времени перезаписи ячейки EEPROM.

Напрашивающийся вывод: максимально использовать обычные функции и переменные, а если очень хочется классов - то статические методы. После замены HardwareSerial на SingleSerial, отказа от использования класса EEPROM и объявлении методов класса SPI статическими объем программы OSD уменьшился на 2 килобайта (из 31к доступных!)

3. GCC, используемый в среде Ардуино, дает совершенно отвратительный код. Просто потому что он рассчитан на мощные 32-бит процессоры, и оптимизацию делает "в понятиях" их возможностей - а затем уже просто собирает шаблоны исполнения этих операций. И вот для AVR в подавляющем большинстве случаев такой шаблон состоит из нескольких (и до нескольких десятков!) машинных команд, для которых уже не будет никакой оптимизации. Родной компилятор от AVR Studio с процессором знакОм лучше и оптимизирует код с учетом содержания регистров - в результате получая выигрыш до 40%.

Несколько примеров.

osdbuf[bufpos++] = c; 

превращается в

lds   r24, 0x0274
lds   r25, 0x0275
movw   r18, r24
subi   r18, 0xFF // команду addiw еще не изобрели?
sbci   r19, 0xFF
sts   0x0275, r19
sts   0x0274, r18
movw   r30, r24 // почему бы сразу не загрузить в R30 и  избавиться от лишних перемещений?
subi   r30, 0x8A
sbci   r31, 0xFD
st   Z, r22

Вот так как с куста 2 совершенно лишние команды. А вот еще лучше

// while (sleep_periods >= 512)  // всего-то 2-байтовое целое сравнить с константой

ld   r16, 0x00
ldi   r17, 0x00
movw   r24, r16
add   r24, r14
adc   r25, r15
cp     r24, r1
sbci r25, 0x02
brcs   .+26

Выделенное цветом - это что вообще было? Наф(зачеркнуто) какого дьявола перед сравнением надо было прибавлять 0, да еще погоняв по регистрам???

Или вот такой вот шедевЕр "оптимизации"

// uint8_t TCCR1Bcopy = TCCR1B; // Сохраним копию регистра
ldi   r26, 0x81 // адрес регистра
mov   r2, r26 // в паре r2r3 получаем 16-бит адрес
mov   r3, r1 
movw   r30, r2 // перегоняем в регистровую пару Z
ld   r18, Z // и вот наконец-то после долгих мучений загрузили регистр.

А достаточно было сказать "lds   r18, 0x81". 

С форматированным выводом вообще ад и израиль,  строчка "osd.printf_P(PSTR("No mavlink data!"));" превращается в кучу шевелений стека.

ldi r24, 0xCB ; 203
ldi r25, 0x01 ; 1
push r25
push r24
ldi r24, 0xB7 ; 183
ldi r25, 0x05 ; 5
push r25
push r24
call 0x4d28 ; 0x4d28 <_ZN12BetterStream9_printf_PEPKcz>
pop r0
pop r0
pop r0
pop r0
ret

Ранее вроде бы все параметры вызовов передавались через регистры - а тут вдруг через стек. А аргументов-то и нету, так что использование print_P вместо printf_P экономит аж 16 байт.

 Мораль - если проект перестал лезть во флеш, то пора брать в руки avr-objdump и смотреть, что там наг%8№кодил компилятор... Ну и по максимуму выносить все похожие вычисления с long и float в отдельные функции - тогда может и удастся "впихнуть невпихнуемое" :)

 

 

Поделиться:
  • Добавить ВКонтакте заметку об этой странице
  • Мой Мир
  • Facebook
  • Twitter
  • LiveJournal
  • В закладки Google
  • Яндекс.Закладки
  • Reddit
  • БобрДобр
  • MisterWong.RU
  • Memori.ru
  • МоёМесто.ru
  • Сто закладок
  • Блог Li.ру
  • Блог Я.ру
  • Одноклассники
    • Замена компилятора GCC с версии 4.8.1, идущего в комплекте с Ардуино, на собранный по инструкции  5.3 с включенным LTO, устранила практически все претензии к качеству кода, сократив расход флеша в неком проекте с 31902 байт до 29064 байт, и что  самое удивительное - увеличив размер свободной памяти с 414 до 490 байт (как???).

      То есть - такая замена насущно рекомендуется.

-
Рейтинг@Mail.ru