ARM аccемблер

    Привет всем!
    По роду деятельности я программист на 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

    2.a1-a4

    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 ассемблера

    В данной таблице я собрал основные инструкции, которая потребуется для дальнейшей разработки (на самом базовом этапе:):
    Название Синтаксис Применение
    ADD (добавление) ADD r0, r1, r2
    r0 = r1 + r2
    SUB (вычитание) SUB r0, r1, r2
    r0 = r1 — r2
    RSB (обратное вычитание) RSB r0, r1, #10
    r0 = 10 — r1
    MUL (умножение) MUL r0, r1, r2
    r0 = r1 * r2
    MOV MOV r0, r1
    r0 = r1
    ORR( логическая операция) ORR r0, r1, r2
    r0 = r1 | r2
    TEQ TEQ r0, r1
    r0 == r1
    LDR (загрузка) LDR r4, [r5]
    r4 = *r5
    STR STR r4, [r5]
    *r5 = r4
    ADR ADR r3, a
    a — переменная. r3 = &a


    Чтобы закрепить использование основных инструкций давайте напишем несколько простых примеров, но сначала нам понадобится 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
    

    На выходе вы получите что-то вроде этого:

    [anton@localhost ~]$ 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 ассемблер. Надеюсь вам понравилось! Этого хватит для того, чтобы далее углубляться в дебри этого языка и писать на нем программы. Если все получится, буду писать дальше о том, что узнаю сам. Если есть ошибки, прошу не пинать, так как я новичок в ассемблере.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 26
    • +2
      Спасибо за статью, интересно, я когда-то читал подобное, но не смог определиться с задачами. Можете описать задачи, которые вы решали, или вы бы решали при помощи ассемблера для ARM?
      • 0
        Я еще учусь, так что ничего серьезного не делал, но та цель ради которой я начал учить ассемблер — оптимизация аудио библиотек, вроде sox под arm. Вы бы видели сколько ассемблерных вставокв том же ffmpeg, без которых он бы сильно грузил процессор на любом arm устройстве.
        • 0
          Раз хотите оптимизировать цифровую обработку сигналов, глядите в сторону Neon инструкций, они позволяют выполнять многие математические операции над группой операндов. фактически можно получить ускорение в разы, если алгоритмы распараллеливаются. В NDK кстати они тоже поддерживаются. особую проблему может создать настройка среды для вывода значений регистров neon'а, но по этой теме уже есть русскоязычные статьи.
          • 0
            Да, спасибо я об этом слышал. Сначала разберусь с основами арм ассемблера вообще, чтоя уже делаю, а потом займусь неон. Может и статья выйдет новая:)
          • 0
            Вы, кстати, про существование orc в курсе? (см. code.entropywave.com/projects/orc/ )
            Может лучше его допиливать вместо того чтоб ассемблерные вставки в библиотеках делать?
        • +1
          О, новый виток развития! С нового года будет чем заниматься
          • +1
            Не любая инструкция может выполнятся условно, например вызов функции (bl, blx) или предзагрузка в память (pld).
            С предзагрузкой, кстати есть много фокусов, типа fast memcpy, который на объёмных данных работает раза в полтора-два быстрее чем обычный.
            • 0
              Спасибо за замечание. Прийму к сведенью.
            • 0
              Спасибо, познавательная статья. Тоже Java программист, кстати. Junior.
              • –2
                В избранное, однозначно!
              • +1
                Спасибо за статью, ассемблер ARM у меня наверное самый любимый.
                • +2
                  Архитектура развивалась с течением времени, и начиная с ARMv7 были определены 3 профиля: ‘A’(application) — приложения, ‘R’(real time) — в реальном времени,’M’(microcontroller) — микроконтроллер. Историю разработки этой технологии и другие интересный данные вы можете прочитать в Википедии или погуглив в интернете. ARM поддерживает разные режимы работы (в основном Thumb и ARM).

                  На самом деле вы не совсем правы в данном моменте, вы начали говорить про ARMv7(не путать с ARM7), то есть ядро Cortex M0-3-4, за 0 точно не скажу, но в M3/4 еспользуется набор инструкций Thumb-2, по сути смесь ARM+Thumb
                  • +1
                    Вообще в M0 тоже Thumb-2, тк все его инструкции включены в M3/M4
                    • 0
                      Спасибо, откорректировал статью.
                      • +1
                        Спасибо, за статью.
                  • 0
                    Также, надо понимать, что удастся симулировать только работу ядра, без доступа к периферии + советую смотреть уже в сторону Cortex вместо ARM7TDMI — там более совершенная система прерываний, одна система команд + контроллер прерываний и 1 таймер входят в состав ядра(одни для всех производителей)
                    • 0
                      >Также, надо понимать, что удастся симулировать только работу ядра

                      Да, это ясно. Я вообще думаю в дальнейшем, если хорошо разбирусь, использовать это для оптимизации библиотек, которые я использую в Android NDK (думаю из статьи понятно:), так что там с периферией разберусь.
                    • 0
                      Спасибо за статью, как раз в универе начинаем изучать программирование ARM'ов.
                      • 0
                        Спасибо. А вот такой вопрос, чем (для программиста) отличаются ARMv4, ARMv7 и др.? Один и тот же листинг ассемблера будет адекватно компилироваться подо все эти платформы? А бинарный код совместим?
                        • 0
                          Ну. Там есть обратная совместимость для приложений.
                          • 0
                            Теоретически будет работать одинаково в разных версиях. Единственная разница — если вы будете использовать, например, NEON в ARMv6, то ничего это вам не даст, тк NEON в старых ARM нет (и в некоторых новых тоже нет). Основные инструкции, конечно будут работать везде.
                          • 0
                            Это, конечно, хорошо, что всё больше людей начинают изучать ARM (хотя, может и нет, с учётом любви вендоров лочить arm-устройства под себя), но вопрос: а с чего Вы решили, что библиотеки не оптимизированы для ARM? Вы пробовали их откомпилировать с оптимизацией, хорошим компилятором? Вроде, и Clang и GCC последних версий весьма недурно справляются с оптимизацией под ARM-ы.

                            Скорее всего, у Вас проблемы из-за того, что библиотеки были скомпилированы под какой-нибудь уж совсем-совсем Generic ABI, поэтому в них не используется плавающая точка или ещё что-нибудь.
                            • 0
                              Вообще я не уверен, что эта библиотека под ARM не оптимизирована, но у меня не получилось ее собрать так, чтобы она не гразилда процессор до 80-90%, да и плюс к тому, как я уже писал — я пеисал под Android, а в нем сборка библиотек идет через Android NDK, поэтому я не могу использовать другие компиляторы (я так думаю, если ошибаюсь — поправьте).

                              А насчет изучения — в любом случае это интернесно, так что время, я считаю, зря не потратил:)
                              • 0
                                В NDK можно отдельно компилировать под ARMv6 и ARMv7, соответственно, с разными оптимизациями (NEON тот же, и пр). Хотя думаю, уж в курсе. Просто странно, что результат не устроил. Может, в коде дело?)
                                • 0
                                  Да, я пробовал оба варианта. Тестировал на Samsung galaxy tab. Если интересно, я писал статью об этом. Вот: habrahabr.ru/blogs/android_development/131377/. Можете сами пропробовать. Конечно есть вероятность, что я сделал что-то не верно, но я больше склоняюсь к тому, что SoX действительно не оптимизирован (а кроме него еще и стопка кодеков, так как этот самый SoX использует штук 10 библиотек, хотя проще было бы одним FFmpeg'ом все декодиить).
                                  • 0
                                    Статью читал, позже из интереса попробую собрать и проверить у себя, но это будет явно в следующем году.
                                    1. Сами пишите, что сам SoX не оптимизирован под ARM. Тем более, в декодировании участвуют и ffmpeg, и другие кодеки, а это возможный источник дополнительных тормозов и пр.
                                    2. Интересно, как смотрите нагрузку. Бинарные модули работают в окружении приложения и в top'е нагрузка указывается как от самого приложения.
                                    3. По поводу компиляции. Средства компилирования NDK (make-файлы) лишь автоматизируют сборку. Наверняка можно также, как в статье, руками выполнить обычную сборку с необходимыми оптимизациями.

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