[Перевод] 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
Такая инструкция идеально подходит для архитектуры 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.
Этот пост изначально начинался как тред в твиттере.