Нередко функция изменяет значения используемых в ней регистров. И может возникнуть необходимость сохранить эти значения внутри функции для последующего локального использования. Особенно если внутри функции эти регистры часто меняют значения. Для управления данными, их сохранения и восстановления применяется фрейм стека. Фрейм стека представляет часть стека, который предназначен для для конкретного вызова функции. Нередко во фрейм стека помещаются параметры функции, ее результат и дополнительные промежуточные данные, которые используются внутри функции (аналоги локальных переменных и констант в языках высокого уровня). Особенно это актуально, если функция использует и вынуждена хранить множество промежуточных данных (локальных переменных). И хотя архитектура ARM64 предоставляет довольно много регистров обзего назначения - Х0-Х30, но это количество все таки то же ограничено, тем более регистры лучше задействовать непосредственно для вычислений, нежели для хранения данных.
Рассмотрим простейший пример:
.global _start _start: mov x0, #10 bl sum mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux sum: sub sp, sp, #16 // выделяем место в стеке для хранения параметров и локальной переменной str x0, [sp, #8] // сохраняем в стек значение параметра из регистра Х0 mov x0, #13 // условная локальная переменная str x0, [sp] // сохраняем в стек значение локальной переменной // здесь могут быть действия, которые изменяют Х0 и Х1 ldr x2, [sp, #8] // восстанавливаем значение параметра ldr x3, [sp] // восстанавливаем значение локальной переменной add x0, x2, x3 // основная работа - вычисление суммы add sp, sp, #16 // очищаем фрейм стека на 16 байт ret
В данном случае функция sum принимает один параметр, определяет локальную переменную и вычисляет их сумму. В начале функция загружает в стек значения параметра и переменной.
str x0, [sp, #8] // сохраняем в стек значение параметра из регистра Х0 mov x0, #13 // условная локальная переменная str x0, [sp] // сохраняем в стек значение локальной переменной
Далее в функции могут идти различные действия, которые вовлекают и изменяют значения параметров. Но нам это не важно, потому что мы сохранили нужные нам значения в стек. И когда они нам нужны, мы берем их стека и производим с ними нужные действия - в данном случае вычисляем сумму:
ldr x2, [sp, #8] // восстанавливаем значение параметра ldr x3, [sp] // восстанавливаем значение локальной переменной add x0, x2, x3 // основная работа - вычисление суммы
Стоит отметить, что здесь мы загружаем данные в совсем другие регистры, потому что хранение данных в стеке позволяет уйти от жесткой привязки к регистрам и при необходимости задействовать регистры для вычислений, а не для простого хранения данных.
Для управления фреймом стека в ассемблере ARM64 используется регистр указателя фрейма стека FP (frame pointer). По умолчанию в качестве такого используется регистр X29. Для использования регистра указателя стека FP обычно вначале в стеке выделяется некоторая область для функции с изменением значения SP, затем новое значение SP (адрес области в стеке, на который указывает SP), копируется в FP:
SUB SP, SP, #32 // перемещаем указатель стека на эту свободную область в стеке MOV FP, SP // помещаем в регистр FP текущий адрес, на который указывает SP
Далее можно сохранять и извлекать значения относительно адреса в регистре FP. То есть FP выступает базовым регистром:
STR X0, [FP] // сохраняем в стек значение из регистра X0 STR X1, [FP, #-8] // сохраняем в стек значение из регистра X1 STR X2, [FP, #-16] // сохраняем в стек значение из регистра X2 STR X3, [FP, #-24] // сохраняем в стек значение из регистра X3 // ...................... LDR X3, [FP, #-24] // извлекаем из стека значение в регистр X3 LDR X2, [FP, #-16] // извлекаем из стека значение в регистр X2 LDR X1, [FP, #-8] // извлекаем из стека значение в регистр X1 LDR X0, [FP] // извлекаем из стека значение в регистр X0
Например, перепишем предыдущий пример с использованием регистра FP:
.global _start _start: mov x0, #10 bl sum mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux sum: sub sp, sp, #16 // выделяем место в стеке для хранения параметров и локальной переменной mov fp, sp // указатель фрейма FP указывает на тот же адрес, что и SP str x0, [fp, #8] // сохраняем в стек значение параметра из регистра Х0 mov x0, #13 // условная локальная переменная str x0, [fp] // сохраняем в стек значение локальной переменной // здесь могут быть действия, которые изменяют Х0 и Х1 ldr x2, [fp, #8] // восстанавливаем значение параметра ldr x3, [fp] // восстанавливаем значение локальной переменной add x0, x2, x3 // основная работа - вычисление суммы add sp, sp, #16 // очищаем фрейм стека на 16 байт ret
Стоит быть внимательным, если одна функция вызывает другую функцию в этом случае они обе могут использовать свой фрейм стека и для доступа к нему использовать свое определенное значение в регистре FP. Соответственно, чтобы вызываемая функция не затерла значение FP, его следует сохранять в стек:
.global _start _start: bl main mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux main: str lr, [sp, #-16]! // сохраняем регистр LR/X30 в стек sub sp, sp, #16 // выделяем место в стеке для локальных переменных mov fp, sp // устанавливаем указатель фрейма FP mov x0, #10 // первая локальная переменная str x0, [fp, #8] // сохраняем ее во фрейме стека mov x0, #15 // вторая локальная переменная str x0, [fp] // сохраняем ее во фрейме стека // извлекаем локальные переменные и передаем их в качестве параметров ldr x0, [fp, #8] // первый параметр ldr x1, [fp] // второй параметр bl sum add sp, sp, #16 // очищаем место в стеке ldr lr, [sp], #16 // восстанавливаем регистр X30/LR ret sum: str fp, [sp, #-16]! // сохраняем регистр FP/X29 в стек sub sp, sp, #16 // выделяем 16 байт в стеке mov fp, sp // указатель фрейма FP указывает на SP str x0, [fp, #8] // сохраняем во фрейм стека первый параметр из регистра Х0 str x1, [fp] // сохраняем во фрейм стека второй параметр из регистра Х1 // здесь могут быть действия, которые изменяют Х0 и Х1 ldr x2, [fp, #8] // восстанавливаем значение первого параметра ldr x3, [fp] // восстанавливаем значение второго параметра add x0, x2, x3 // основная вычисление суммы add sp, sp, #16 // очищаем фрейм стека на 16 байт ldr fp, [sp], #16 // восстанавливаем регистр X29/FP ret
Здесь функция main устанавливает локальные переменные, сохраняет их во фрейм стека и затем передает их в качестве параметров в функцию sum. В функции sum оба параметра сохраняются во фрейм стека, и затем вычисляется их сумма. Причем поскольку вызов функции sum меняет значение в регистре X30/LR, то в начале функции main его значение сохраняется в стек, а в конце функции main восстанавливается. Поскольку функция sum также изменяет регистр FP/X29, то данный регистр также сохраняется. Но сохранение происходит в вызываемой функции sum:
str fp, [sp, #-16]! // сохраняем регистр FP/X29 в стек //............. ldr fp, [sp], #16 // восстанавливаем регистр X29/FP
Поскольку работа с регистрами X29/FP и X30/LR является ощим местом функций, то для простоты нередко можно встретить, как в начале функции сохраняются эти регистры, а в конце функции восстанавливаются, даже если в самой функции в реальности эти регистры не используются. Например:
.global _start _start: bl main mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux main: stp x29, x30, [sp, #-32]! // выделяем 32 байта в стеке и сохраняем регистры FP/X29 и LR/X30 add fp, sp, #16 // устанавливаем указатель фрейма FP на SP+16 mov x0, #10 // первая локальная переменная str x0, [fp, #8] // сохраняем ее во фрейме стека mov x0, #15 // вторая локальная переменная str x0, [fp] // сохраняем ее во фрейме стека // извлекаем локальные переменные и передаем их в качестве параметров ldr x0, [fp, #8] // первый параметр ldr x1, [fp] // второй параметр bl sum ldp x29, x30, [sp], #32 // восстанавливаем регистры X29/FP и X30/LR и очищаем стек ret sum: stp x29, x30, [sp, #-32]! // выделяем 32 байта в стеке и сохраняем регистры FP/X29 и LR/X30 add fp, sp, #16 // устанавливаем указатель фрейма FP на SP+16 str x0, [fp, #8] // сохраняем во фрейм стека первый параметр из регистра Х0 str x1, [fp] // сохраняем во фрейм стека второй параметр из регистра Х1 // здесь могут быть действия, которые изменяют Х0 и Х1 ldr x2, [fp, #8] // восстанавливаем значение первого параметра ldr x3, [fp] // восстанавливаем значение второго параметра add x0, x2, x3 // основная вычисление суммы ldp x29, x30, [sp], #32 // восстанавливаем регистры X29/FP и X30/LR и очищаем стек ret
В начале функции (так называемом "прологе" функции) мы видим выделение памяти в стеке на 32 байта и сохранение регистров FP и LR:
stp x29, x30, [sp, #-32]!
После этого в стеке будет выделено 32 байта, а первые 16 байт займут регистры FP и LR. Остальные 16 байт предназначены для двух 8-байтовых локальные переменных/параметров, поэтому для установки указателя стека FP прибавляем к значению в SP 16 байт
В конце функций (в так называемом "эпилоге") восстанавливаем регистры X29/FP и X30/LR и очищаем стек. SP сдвигается на 32 байта вниз.
ldp x29, x30, [sp], #32 // восстанавливаем регистры X29/FP и X30/LR и очищаем стек