Фрейм стека

Последнее обновление: 20.07.2023

Нередко функция изменяет значения используемых в ней регистров. И может возникнуть необходимость сохранить эти значения внутри функции для последующего локального использования. Особенно если внутри функции эти регистры часто меняют значения. Для управления данными, их сохранения и восстановления применяется фрейм стека. Фрейм стека представляет часть стека, который предназначен для для конкретного вызова функции. Нередко во фрейм стека помещаются параметры функции, ее результат и дополнительные промежуточные данные, которые используются внутри функции (аналоги локальных переменных и констант в языках высокого уровня). Особенно это актуально, если функция использует и вынуждена хранить множество промежуточных данных (локальных переменных). И хотя архитектура 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 байт

Фрейм стека stack frame в ассемблере ARM64

В конце функций (в так называемом "эпилоге") восстанавливаем регистры X29/FP и X30/LR и очищаем стек. SP сдвигается на 32 байта вниз.

ldp x29, x30, [sp], #32     // восстанавливаем регистры X29/FP и X30/LR и очищаем стек
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850