Соглашения ARM ABI

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

При вызове функций C/C++ по умолчанию применяются некоторые условности ARM ABI (Application Binary Interface), которые определяюься стандартом AAPCS (Procedure Call Standard for the Arm Architecture). Этот стандарт определяет некоторые условности, которым должны следовать вызывающий функцию код и вызываемая функция, или то что называется соглашением о вызовах функции (calling convention). Например, как в функцию передаются параметры и как она возвращает результат, а также ряд дополнительных моментов - какие регистры могут свободно изменяться, а какие должны быть сохранены во время вызова и т.д. Конкретные вендоры не обязаны следовать этим условностям, однако в целом они соблюдаются на всех основных платформах (Windows, Linux, MacOS) и разделяются сообществом разработчиков. Основные моменты этих условностей:

  • Значения для первых 8 целочисленных параметров передаются последовательно через регистры X0-X7, то есть первый параметр в Х0, второй параметр - в Х1, ... восьмой параметр через регистр Х7. Указатели представляют 8-байтное целочисленное значение, поэтому также передаются через эти регистры.

  • Значения для первых 8 параметров, которые представляют числа с плавающей точкой, передаются последовательно через регистры V0-V7

  • 9-й и последующие параметры передаются через стек

  • Данные, размер которых превышает размер регистра, разбиваются на несколько регистров (при их доступности). Например, 128-битное число передается через два последовательных регистра Хn.

  • Целочисленный результат помещается в Х0, результат-число с плавающей точкой - в V0. Если результат представляет 128-битное число, то оно занимает два регистра - Х0 и Х1.

  • Для возвращения из функции результата, который представляет адрес, применяется регистр X8

  • Регистры, которые функция может свободно изменять при вызове: X0-X15. Их еще называют volatile registers или временные регистры. Если же по логике программы надо сохранить подобные регистры, то это делает вызывающий функцию код

  • Так называемые неизменяемые регистры или nonvolatile registers: X19-X30

    . Если функция изменяет значения в подобных регистрах, то перед их изменением эта функция должна сохранить эти значения. А при возврате из функции восстановить сохраненные значения обратно в регистры. То есть эти регистры сохраняет вызываемый код
  • Регистр указателя фрейма стека: X29

  • Регистр для хранения адреса возврата из функции: X30

  • Регистр указателя стека: SP

Например, определим в папке приложения файл sum.c со следующим кодом на языке С:

#include <stdio.h>

int sum(int a, int b){
    printf("a=%d \n", a);
    printf("b=%d \n", b);
    int result = a + b;
    printf("a + b = %d \n", result);
    return result;
}

Здесь определена функция sum, которая получает два числа через параметры и возвращает их сумму, попутно логируя значения параметров и результата.

Также в той же папке определим файл app.s с кодом на ассемблере, который использует функцию sum:

.global main
 
main:                       // функция main
    str lr,[sp,#-16]!       // сохраняем в стеке текущий адрес из регистра lr
    mov x0, #10             // a = 10
    mov x1, #12             // b = 12
    bl sum                  // вызываем функцию sum
    ldr lr, [sp], #16       // извлекаем из стека адрес в регистр lr
    ret                     // выходим из функции

Функция sum принимает два параметра. Для первого параметра в регистр Х0 помещаем число 10, для второго параметра в регистр Х1 помещаем число 12 и затем вызываем функцию sum. После ее вызова мы ожидаем, что в Х0 будет ее результат. Скомпилируем приложение. Далее я буду приводить пример компиляции на Linux ARM64:

gcc app.s sum.c -o app

Или с помощью команды (если применяется aarch64-none-linux-gnu-gcc):

aarch64-none-linux-gnu-gcc app.s sum.c -o app -static

Затем запустим приложение:

eugene@Eugene:~/arm$ gcc app.s sum.c -o app
eugene@Eugene:~/arm$ ./app
a=10
b=12
a + b = 22
eugene@Eugene:~/arm$ echo $?
22
eugene@Eugene:~/arm$

После вызова команды echo $? консоль отображает значение регистра Х0 - число 22.

Но это был очень простой пример. Рассмотрим более сложный случай. Пусть у нас определена следующая функция sum:

#include 

int sum(int x0, int x1, int x2, int x3, int x4, int x5, int x6, int x7, char x8, int x9){
    printf("x0=%d \n", x0);
    printf("x1=%d \n", x1);
    printf("x2=%d \n", x2);
    printf("x3=%d \n", x3);
    printf("x4=%d \n", x4);
    printf("x5=%d \n", x5);
    printf("x6=%d \n", x6);
    printf("x7=%d \n", x7);
    printf("x8=%d \n", x8);
    printf("x9=%d \n", x9);
    int result = x0 + x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9;
    printf("sum=%d \n", result);
    return result;
}

Для простоты почти все параметры называются по имени регистров, которые передают для этих параметров значения. Кроме последних двух. Поскольку функция принимает 10 параметров, то последние два параметра будут передаваться через стек.

Для вызова этой функции определим следующий код на ассемблере:

.global main
 
main:                       // функция main
    str lr,[sp,#-16]!       // сохраняем в стеке текущий адрес из регистра lr
    mov x0, #0             
    mov x1, #1             
    mov x2, #2             
    mov x3, #3             
    mov x4, #4            
    mov x5, #5           
    mov x6, #6            
    mov x7, #7            
    mov x10, #10            
    mov x11, #11 
    stp x10, x11, [sp, #-16]!   // помещаем в стек значения для последних двух параметров
    bl sum                  // вызываем функцию sum
    add sp, sp, #16         // удаляем ранее выделенную память для параметров
    ldr lr, [sp], #16       // извлекаем из стека адрес в регистр lr
    ret                     // выходим из функции

Передача значений для первых 8 параметров довольно проста - устанавливаем регистры Х0-Х7. Для последних двух параметров значения хранятся в регистрах Х10 и Х11. Но чтобы функция их получила помещаем их в стек:

stp x10, x11, [sp, #-16]!

Данное сохранение мы могли бы переписать следующим образом:

sub sp, sp, #16
str x10, [sp]           // сохраняем Х10 по адресу SP
str x11, [sp, #8]       // сохраняем Х11 по адресу SP + 8

То есть на верхушке стека будет хранится число 10 из регистра Х10, которое передается предпоследнему параметру функции sum. По адресу SP+8 хранится значение регистра Х11, которое передается последнему параметру.

Причем обратите внимание, что предпоследний параметр функции sum представляет тип char, то есть число размер в 1 байт. Но для него все равно будут использоваться первые 8 байт из стека. Таким образом, 9-й параметр получит значение по адресу в SP, 10-й параметр - по адресу в SP+8, 11-й параметр - по адресу SP+16 и т.д.

Результат работы программы:

eugene@Eugene:~/arm$ gcc app.s sum.c -o app
eugene@Eugene:~/arm$ ./app
x0=0
x1=1
x2=2
x3=3
x4=4
x5=5
x6=6
x7=7
x8=10
x9=11
sum=49
eugene@Eugene:~/arm$ echo $?
49
eugene@Eugene:~/arm$
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850