Функции могут принимать параметры и возвращать некоторые значения. Обычно для передачи параметров применяются регистры. Например, первый параметр помещается в регистр X0, второй параметр - в X1 и т.д. Поскольку количество регистров ограничено, и они могут использоваться для других целей, также можно передавать значения параметров через стек. Можно комбинировать различные подходы.
Возвращаемое значение обычно помещается в один из регистров - обычно в X0. Если данные не вмещаются в один регистр, например, 128-битное число, то можно использовать несколько регистов, например, X0 и X1.
Вне зависимости от выбранного подхода необходимо следить, чтобы при вызове функции не было затирания значений регистров, которые могут быть предназначены для других целей.
Рассмотрим простейший пример - функцию, которая складывает два числа:
.global _start _start: MOV X0, #5 // первый параметр MOV X1, #6 // второй параметр BL sum // вызываем функцию sum MOV X8, #93 // устанавливаем функцию Linux для выхода из программы SVC 0 // Вызываем функцию Linux // Функция sum сладывает два числа // Параметры: // X0 - первое число // X1 - второе число sum: ADD X0, X0, X1 RET
Функция sum использует инструкцию ADD для сложения двух чисел, которые располагаются в регистрах Х0 и Х1.
В основной программе перед вызовом функции sum устанавливаются значения для этих параметров:
MOV X0, #5 // первый параметр MOV X1, #6 // второй параметр BL sum // вызываем функцию sum
Стоит отметить, что есть различные соглашения по поводу того, как следует передавать параметры. Наиболее распространенный в данном случае стандарт - стандарт AAPCS (Procedure Call Standard for the Arm Architecture) устанавливает, что для передачи целочисленных параметров в функцию используются регистры X0-X7. Указатели (адреса) представляют 8-байтное целочисленное значение, поэтому также передаются через эти регистры. Для передачи чисел с плавающей точкой используются регистры V0-V7. Данные, размер которых превышает размер регистра, разбиваются на несколько регистров (при их доступности) или передаются через стек.
Рассмотрим более сложный, но более практичный пример - копирование одной строки в другую:
// METANIT.COM. Определение функции с параметрами .global _start _start: LDR X0, =input // строка, их которой надо скопировать символы LDR X1, =output // строка, в которую надо скопировать символы BL copy // вызов функции copy MOV X0, 0 // код возврата - 0 MOV X8, #93 // устанавливаем функцию Linux для выхода из программы SVC 0 // Вызываем функцию Linux // определение функции copy, которая копирует символы из одной строки в другую // X0 - адрес входящей строки // X1 - адрес исходящей строки copy: MOV X4, X1 // сохраняем адрес начала строки, чтобы потом вычислить ее длину // в цикле получаем все байты, пока не дойдем до нулевого байта loop: LDRB W5, [X0], #1 // загружаем из X0 один байт - один символ в W5 и увеличиваем адрес в X0 на 1 байт CMP W5, #0 // сравниваем с нулевым байтом B.EQ endloop // если нулевой байт, переход к метке endloop STRB W5, [X1], #1 // если символы равны, заменяем байт по адресу X1 и увеличиваем адрес в X1 на 1 байт B loop // перед обратно к метке loop // печать строки на консоль endloop: SUB X2, X1, X4 // длина строки X2 = X1 - X4 MOV X1, X4 // в X4 сохранен начальный адрес генерируемой строки MOV X0, #1 // 1 = StdOut - стандартный поток вывода MOV X8, #64 // функция Linux для вывода в поток SVC 0 // вызываем функцию Linux RET // выход из функции .data input: .asciz "Hello METANIT.COM!\n" output: .fill 20, 1, 0
Здесь определена функция copy
, которая принимает два параметра - строку, из которой надо скопировать символы, и строку, в которую надо скопировать символы. Адрес
исходной строки передается через регистр X0, а адрес генерируемой строки передается через регистр X1.
Здесь мы предполагаем, что строка завершается нулевым байтом. Для этого определяем исходную строку с помощью директивы .asciz. Строку, в которую надо скопировать определяется фактически как набор из 20 байт, которые заполнены нулями. Кроме того, здесь подразумевается, что каждый символ строки равен 1 байту (то есть кириллица не пройдет). Стоит отметить, что мы в данном случае не проверяем соответствие строк по длине и предполагаем, что вторая строка достаточно большая, чтобы вместить все символы из первой строки.
В самой функции, поскольку мы сами будет вычислять длину строки, сохраняем начальный адрес строки из X1 в регистр X4:
MOV X4, X1
Далее в цикле загружаем каждый байт по адресу X1 в регистр W5:
LDRB W5, [X0], #1
При этом адрес в X0 увеличивается на 1 байт, чтобы в следующий раз мы взяли следующий символ.
Затем сравниваем символ с нулевым байтом, и если равенство верно, завершаем цикл и переходим к метке endloop
CMP W5, #0 // сравниваем с нулевым байтом B.EQ endloop
Если байт не нулевой, копируем во вторую строку по адресу, который хранится в X1
STRB W5, [X1], #1 // если символы равны, заменяем байт по адресу X1 и увеличиваем адрес в X1 на 1 байт B loop
При этом увеличиваем значение адреса на 1, чтобы в следующий раз положить байт по следующему адресу. И переходим обратно к метке loop.
Поскольку в X4 сохранен начальный адрес, а в X1 после добавления символов и приращений адреса хранится конечный адрес, то мы можем получить длину строки вычитанием:
SUB X2, X1, X4 // длина строки X2 = X1 - X4
И в конце функции выводим полученную строку на консоль.
В общей части программы загружаем адреса строк в регистры и вызываем функцию copy
LDR X0, =input // строка, их которой надо скопировать символы LDR X1, =output // строка, в которую надо скопировать символы BL copy // вызов функции copy
Другой пример - замена в строке одного символа на другой:
.global _start _start: LDR X0, =input // строка для вывода на экран MOV X1, #'l' // символ, который надо заменить MOV X2, #'*' // символ, на который надо заменить BL replace // вызов функции replace MOV X0, 0 // код возврата - 0 MOV X8, #93 // устанавливаем функцию Linux для выхода из программы SVC 0 // Вызываем функцию Linux // определение функции replace, которая заменяет один символ на другой // X0 - адрес строки // W1 - символ, который надо заменить // W2 - символ, на который надо заменить replace: MOV X4, X0 // сохраняем адрес начала строки, чтобы потом вычислить ее длину // в цикле получаем все байты, пока не дойдем до нулевого байта loop: LDRB W5, [X0], #1 // загружаем из X0 один байт - один символ в W5 CMP W5, W1 // сраниваем с символом, который надо заменить B.NE ifzero // если символы НЕ равны, то переходим для проверки нулевого байта на метку ifzero STRB W2, [X0, #-1] // если символы равны, заменяем байт по адресу X0-1 ifzero: CMP X5, #0 // сравниваем с нулевым байтом B.NE loop // если НЕнулевой байт, переход обратно к метке loop // печать строки на консоль MOV X1, X4 // в X4 сохранен начальный адрес строки SUB X2, X0, X4 // длина строки X2 = X0 - X4 MOV X0, #1 // 1 = StdOut - стандартный поток вывода MOV X8, #64 // функция Linux для вывода в поток SVC 0 // вызываем функцию Linux RET // выход из функции .data input: .asciz "Hello World!\n"
Здесь функция replace
принимает три параметра:
В регистр X0 помещается адрес строки, где надо выполнить замену.
В регистр W1 - символ, который надо заменить.
В регистр W2 - символ, на который надо заменить.
В данном случае строка состоит из однобайтных символов и заканчивается нулевым байтом. В самой функции считываем каждый символ в регистр W5, сравниваем его с символом, который надо заменить:
CMP W5, W1 STRB W2, [X0, #-1] // если символы равны, заменяем байт по адресу X0-1
Если символы совпадают, то заменяем данный символ в строке на символ из регистра W2. Поскольку при считывании символа мы увеличили значение адреса в регистре X0 на 1 байт, то при замене символа вычитаем один байт.
При вызове функции устанавливаем три вышеуказанных параметра:
LDR X0, =input // строка для вывода на экран MOV X1, #'l' // символ, который надо заменить MOV X2, #'*' // символ, на который надо заменить BL replace // вызов функции replace
Таким образом, в строке "Hello World!\n" заменяем символ "l" на символ "*", поэтому в итоге получим строку "He**o Wor*d!\n"