[Перевод] LDM. Моя любимая инструкция ARM

LDM — или load multiple — моя любимая инструкция в ассемблере для ARM. Вот почему.

Во-первых, что она делает. Вот пример:

ldm r4, {r0, r1, r2, r3}


Здесь она принимает базовый регистр (в данном случае r4) и набор регистров (в данном случае {r0, r1, r2, r3}). Загружает последовательные слова из адреса в базовом регистре в регистры из набора. Действие инструкции можно продемонстрировать с помощью такого C-подобного псевдокода:

r0 = r4[0];
r1 = r4[1];
r2 = r4[2];
r3 = r4[3];


Немало заданий для одной инструкции! Именно поэтому она называется load multiple.

Нотация набора также допускает диапазоны. Мы можем переписать предыдущий пример следующим образом:

ldm r4, {r0-r3}


В наборе разрешены все 16 регистров ARM. Итак, законно следующее:

ldm r0, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15}


В 32-битной инструкции набор регистров кодируется как 16-битная маска. Вот упрощённая кодировка исходного примера:

Упрощённое кодирование инструкции LDM
ffjgz8mnfxnkj0ykhrlieqax-si.png

Такая инструкция идеально подходит для архитектуры load-store, такой как ARM, где основной рабочий процесс выглядит так:

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


Противоположностью LDM находится STM — store multiple.
С помощью этих двух инструкций можно быстро копировать большие блоки памяти. Вы можете скопировать восемь слов (или 32 байта!) памяти всего двумя инструкциями:

ldm r0, {r4-r11}
stm r1, {r4-r11}


У LDM и STM также есть варианты автоинкремента (обозначаемые знаком »!»), где базовый регистр увеличивается на количество загруженных/сохранённых слов, так что можно копировать в быстром цикле:

ldm r0!, {r4-r11}
stm r1!, {r4-r11}


В ARM инструкция POP — это просто псевдоним для LDM с указателем стека (и автоинкрементом). Она выглядит и работает точно так же:

ldm sp!, {r0-r3}
pop {r0-r3}


А инструкция PUSH — это псевдоним для варианта STM (STMDB).

Вы можете делать push и pop, копируя регистры в большом объёме в стек и из него за один раз. А если заменить SP другим регистром, то сможете реализовать эффективные стеки в других областях памяти. Например, вы можете реализовать теневой стек в куче.


Вы боитесь использовать регистры, сохранённые вызовом (call-preserved), потому что их требуется сохранить, а в любом случае можно использовать слот стека? Это больше не проблема, потому что можно за один раз сохранить все call-preserved регистры, которые вы хотите использовать:

push {r4-r11}


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

push {fp, lr}


А потом восстановить их и вернуться (для эпилога):

pop {fp, lr}
bx lr


Ещё лучше, вы можете всё восстановить и вернуться за один раз!

pop {fp, pc}


Тут значение обратного адреса (LR) вставляется в регистр счётчика программ (PC), так что не нужен явный возврат!

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

push {r0-r3, fp, lr}


Или можно сохранить FP и LR и одновременно выделить некоторое пространство в стеке:

push {r0-r3, fp, lr}


В этом случае мы пушим r0-r3 не для их значения, а чтобы продвинуть указатель стека на четыре слова.
Подозреваю, когда пришло время разработать 64-битную версию набора команд ARM, пришлось пойти на компромисс и удвоить количество регистров до 32-х. Помню, в какой-то статье говорилось, что это изменение улучшает производительность примерно на 6% по всем направлениям. С 32-мя регистрами больше невозможно закодировать битовую маску всех регистров в 32-битную длинную инструкцию. Таким образом, вместо ARM64 появились LDP и STP: load pair и store pair, духовные преемники LDM и STM.

Этот пост изначально начинался как тред в твиттере.

© Habrahabr.ru