LDM — или load multiple — моя любимая инструкция в ассемблере для ARM. Вот почему.
Во-первых, что она делает. Вот пример:
Здесь она принимает базовый регистр (в данном случае
Немало заданий для одной инструкции! Именно поэтому она называется load multiple.
Нотация набора также допускает диапазоны. Мы можем переписать предыдущий пример следующим образом:
В наборе разрешены все 16 регистров ARM. Итак, законно следующее:
В 32-битной инструкции набор регистров кодируется как 16-битная маска. Вот упрощённая кодировка исходного примера:
Упрощённое кодирование инструкции LDM
Такая инструкция идеально подходит для архитектуры load-store, такой как ARM, где основной рабочий процесс выглядит так:
Противоположностью LDM находится STM — store multiple.
С помощью этих двух инструкций можно быстро копировать большие блоки памяти. Вы можете скопировать восемь слов (или 32 байта!) памяти всего двумя инструкциями:
У LDM и STM также есть варианты автоинкремента (обозначаемые знаком “
В ARM инструкция POP — это просто псевдоним для LDM с указателем стека (и автоинкрементом). Она выглядит и работает точно так же:
А инструкция PUSH — это псевдоним для варианта STM (STMDB).
Вы можете делать push и pop, копируя регистры в большом объёме в стек и из него за один раз. А если заменить SP другим регистром, то сможете реализовать эффективные стеки в других областях памяти. Например, вы можете реализовать теневой стек в куче.
Вы боитесь использовать регистры, сохранённые вызовом (call-preserved), потому что их требуется сохранить, а в любом случае можно использовать слот стека? Это больше не проблема, потому что можно за один раз сохранить все call-preserved регистры, которые вы хотите использовать:
В ARM первые четыре аргумента, обратный адрес (LR) и указатель кадра (FP) передаются в регистрах. Вот почему особенно важно иметь эффективные пролог и эпилог. К счастью, вы можете сохранить FP и LR за один раз, используя довольно стандартный пролог ARM:
А потом восстановить их и вернуться (для эпилога):
Ещё лучше, вы можете всё восстановить и вернуться за один раз!
Тут значение обратного адреса (LR) вставляется в регистр счётчика программ (PC), так что не нужен явный возврат!
Это достаточно хорошо само по себе, но можно одновременно отправить некоторые аргументы в стек (например, если их адрес занят):
Или можно сохранить FP и LR и одновременно выделить некоторое пространство в стеке:
В этом случае мы пушим
Подозреваю, когда пришло время разработать 64-битную версию набора команд ARM, пришлось пойти на компромисс и удвоить количество регистров до 32-х. Помню, в какой-то статье говорилось, что это изменение улучшает производительность примерно на 6% по всем направлениям. С 32-мя регистрами больше невозможно закодировать битовую маску всех регистров в 32-битную длинную инструкцию. Таким образом, вместо ARM64 появились LDP и STP: load pair и store pair, духовные преемники LDM и STM.
Этот пост изначально начинался как тред в твиттере.
Во-первых, что она делает. Вот пример:
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
не для их значения, а чтобы продвинуть указатель стека на четыре слова.ARM64
Подозреваю, когда пришло время разработать 64-битную версию набора команд ARM, пришлось пойти на компромисс и удвоить количество регистров до 32-х. Помню, в какой-то статье говорилось, что это изменение улучшает производительность примерно на 6% по всем направлениям. С 32-мя регистрами больше невозможно закодировать битовую маску всех регистров в 32-битную длинную инструкцию. Таким образом, вместо ARM64 появились LDP и STP: load pair и store pair, духовные преемники LDM и STM.
Этот пост изначально начинался как тред в твиттере.