Функции

Определение и вызов функции

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

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

В ассемблере ARM64 для вызова функции применяется инструкция перехода BL (branch with link), которая выполняет переход и помещает адрес следующей инструкции, которая идет после BL, в регистр LR (link register, он же регистр X30).

BL func     // переход к метке func, которая представляет функцию.

Однако здесь есть еще одна проблема - необходимо возвратиться из функции. Для возврата в вызывающий код внутри функции применяется инструкция RET (return).

func: 
    // действия функции - различные инструкции
    RET     // выход из функции

Когда функция завершена, и в ней выполняется инструкция RET, данная инструкция выполняет копирование адреса из регистра LR/X30 обратно в регистр PC. Благодаря этому после завершения функции программа перейдет к инструкции, которая идет вслед за вызовом функции (то есть после инструкции BL).

Стоит отметить, что RET в некотором смысле равноценно вызову BR X30, то есть переходу к адресу, который хранится в регистре X30.

Рассмотрим пример определения и вызова функции:

// METANIT.COM. Пример определения и вызова функции
.global _start 
_start: 
    BL print            // вызываем функцию print

    MOV X0, 5          // для теста устанавливаем код возврата - 5
    MOV X8, #93       // устанавливаем функцию Linux для выхода из программы
    SVC 0             // Вызываем функцию Linux

// определение функции print
print:                 
    MOV X0, #1          // 1 = StdOut - поток вывода
    LDR X1, =hello      // строка для вывода на экран
    MOV X2, #19         // длина строки
    MOV X8, #64         // устанавливаем функцию Linux
    SVC 0               // вызываем функцию Linux для вывода строки
    RET

.data
    hello: .ascii "Hello METANIT.COM!\n"

Здесь определяется функция print, которая выводит на консоль строку hello. То есть фактически все определение функции представляет следующий код

print:                 
    MOV X0, #1          // 1 = StdOut - поток вывода
    LDR X1, =hello      // строка для вывода на экран
    MOV X2, #19         // длина строки
    MOV X8, #64         // устанавливаем функцию Linux
    SVC 0               // вызываем функцию Linux для вывода строки
    RET                 // выход из функции

В программе мы можем вызвать эту функцию:

BL print

В итоге при вызове функции произойдет переход к метке print, которая выведет строку на консоль.

Общий процесс вызова:

Вызов функции в ассемблере ARM64
  1. Выполняется инструкция BL print. Она сохраняет в регистр LR адрес следующей инструкции - MOV X0, 5 и переходит к коду функции print.

  2. Функция print завершает свое выполнение с помощью инструкции RET, которая выполняет переход на адрес, сохраненный в регистре LR, то есть на адрес следующей инструкции - MOV X0, 5

Модифицируем пример, чтобы функция print могли печатать разные строки:

.global _start 
_start: 
    LDR X1, =hello_metanit   // строка для вывода на экран
    MOV X2, #19             // длина строки
    BL print                // вызов функции print

    LDR X1, =hello_world   // строка для вывода на экран
    MOV X2, #12           // длина строки
    BL print            // вызов функции print

    MOV X0, 0         // код возврата - 0
    MOV X8, #93       // устанавливаем функцию Linux для выхода из программы
    SVC 0             // Вызываем функцию Linux

// определение функции print
print:                 
    MOV X0, #1        // 1 = StdOut - поток вывода
    MOV X8, #64     // устанавливаем функцию Linux
    SVC 0           // вызываем функцию Linux для вывода строки
    RET

.data
    hello_metanit: .ascii "Hello METANIT.COM!\n"
    hello_world: .ascii "Hello World\n"

Здесь два раза вызывается функция print, а конкретные строки и их размер устанавливаются извне до ее вызова.

Вызов функции по адресу в регистре

Для вызова функции также можно использовать инструкцию BLR. В качестве операнда она принимает регистр, который содержит адрес функции. Например:

.global _start 
_start: 
    mov x0, 10
    mov x1, 11
    ldr x2, =sum       // загружаем адрес функции в регистр X2
    blr x2             // вызываем функцию по адресу в X2
    mov x8, #93       // устанавливаем функцию Linux для выхода из программы
    svc 0             // Вызываем функцию Linux

sum:
    add x0, x0, x1
    ret

Здесь с помощью инструкции LDR загружаем в регистр Х2 адрес функции sum, а с помощью инструкции BLR вызываем эту функцию.

Инструкция BLR как и BR обращается по адресу в регистре, только при этом также помещает в регистр LR адрес возврата из функции.

Сохранение регистров

Стоит отметить, что функция может использовать различные регистры, которые также используется во внешнем коде, который вызывает эту функцию. Это может привести, что функция затрет содержание регистров. В этом случае перед вызывом функции следует сохранять значения регистров в стек, а после выполнения функции восстанавливать из стека. Например, рассмотрим следующую ситуацию:

.global _start 
_start: 
    MOV X0, #5          // для теста устанавливаем код возврата - 5
    BL print            // вызов функции print меняет значение X0

    MOV X8, #93       // устанавливаем функцию Linux для выхода из программы
    SVC 0             // Вызываем функцию Linux - X0 = 19

// определение функции print
print:                 
    MOV X0, #1        // 1 = StdOut - поток вывода
    LDR X1, =hello   // строка для вывода на экран
    MOV X2, #19      // длина строки
    MOV X8, #64     // устанавливаем функцию Linux
    SVC 0           // вызываем функцию Linux для вывода строки
    RET

.data
    hello: .ascii "Hello METANIT.COM!\n"

Здесь для теста перед вызовом функции print в регистр Х0 устанавливается код возврата из программы:

MOV X0, #5

Этот код возвращается системной функцией Linux с номером 93 при завершении программы. Однако после установки кода в функции print это значение, во-первых, изменяется:

print:                 
    MOV X0, #1        // 1 = StdOut - поток вывода

Во-вторых, при вызове системной функции Linux с номером 64, которая как раз выводит сообщение на консоль, в регистр Х0 автоматически помещается длина строки. То есть в итоге после вывода функции print в данном случае в регистре Х0 будет находиться число 19.

Но регистр Х0 - это частный случай, так как выполнение функции может вовлекать множество регистров. И в этом случае значения подобных регистров следуюет сохранять в стек:

.global _start 
_start: 
    MOV X0, #5          // для теста устанавливаем код возврата - 5
    STR X0, [SP, #-16]! // сохраняем в стек регистр Х0
    BL print            // вызов функции print меняет значение в регистре X0

    LDR X0, [SP], #16 // восстанавливаем из стека значение регистра X0
    MOV X8, #93       // устанавливаем функцию Linux для выхода из программы
    SVC 0             // Вызываем функцию Linux: X0 = 5

// определение функции print
print:                 
    MOV X0, #1        // 1 = StdOut - поток вывода
    LDR X1, =hello   // строка для вывода на экран
    MOV X2, #19      // длина строки
    MOV X8, #64     // устанавливаем функцию Linux
    SVC 0           // вызываем функцию Linux для вывода строки
    RET

.data
    hello: .ascii "Hello METANIT.COM!\n"

Теперь перед вызовом функции сохраняем регистр Х0 в стек:

STR X0, [SP, #-16]! // сохраняем в стек регистр Х0

А после вызова, наоборот, извлекаем из стека в регистр:

LDR X0, [SP], #16
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850