Функции позволяют организовать код в отдельные независимые единицы, которые при необходимости можно многократно вызывать в течение работы программы. По сути функция представляет некоторые кусок кода, который проецируется на некоторую метку. В зависимости от архитектуры принцип работы функций может немного отличаться.
В ассемблере ARM64 для вызова функции применяется инструкция перехода BL (branch with link), которая выполняет
переход и помещает адрес следующей инструкции, которая идет после BL, в регистр LR (link register, он же регистр X30).
BL func // переход к метке func, которая представляет функцию.
Сама функция представляет метку, после которой идут инструкции и которые завершаются специальной инструкцией RET (return):
bl func // вызов функции
...................................
func:
// действия функции - различные инструкции
ret // выход из функции
Когда функция завершена, и в ней выполняется инструкция RET, данная инструкция выполняет копирование адреса из регистра LR/X30 обратно в регистр PC. Благодаря этому после завершения функции программа перейдет к инструкции, которая идет вслед за вызовом функции (то есть после инструкции BL).
На Intel в зависимости от диалекта ассемблера принцип определения функций и их вызов может отличаться. Но для вызова функции обычно применяется инструкция CALL.
call func ; вызов функции
..................................
func:
; действия функции - различные инструкции
ret ; выход из функции
Инструкция call помещает в стек адрес инструкции, которая идет сразу после вызова - адрес возврата. В конце выполнения функции вызывается инструкция ret. Она извлекает адрес возврата из стека и передает управление на этот адрес.
То есть основное различие между Intel и ARM в данном случае заключается, что на Intel адрес возврата помещается в стек, а в ARM - в специальный регистр LR. В данном случае будем ориентироваться на ARM. Пусть у нас будет инструкция BL, которая в качестве параметра принимает метку - адрес вызываемой функции, и инструкция RET для выхода из функции. Итак, определим следующую программу на языке Python:
lines = [] # строки файла
instructions = [] # иструкции, разбитые по токенам
addr = 0 # адрес инструкции
sym_tab = {} # таблица символов
sp = 8 # указатель стека
stack = [0]*sp # условный стек
# значения 4 регистров
r = [0]*4
# флаги
c = 0 # флаг переноса
n = 0 # флаг знака
z = 0 # флаг нуля
# карта сопоставления регистров и их индексов в списке r
regs32 = {"r0":0, "r1":1, "r2":2, "r3":3}
# поддерживаемые инструкции и их типы
# 1 - инструкции с 1 операндом - меткой
# 2 - инструкции с 2 операндами, где первый операнд - регистр, а второй - регистр или литерал
# 3 - инструкции с 3 операндами, где первый и второй операнды - регистр, а третий - регистр или литерал
# 4 - инструкции с 1 операндом, где операнд может быть регистром или литералом
# 5 - инструкции с 1 операндом, где операнд может быть регистром
# 6 - инструкции без операндов
mnemonics = {"b": (1,1), "beq":(1,1), "bne":(1,1), "bl":(1,1), "mov":(2,2), "cmp":(2,2), "add": (3,3),
"sub":(3, 3), "and": (3,3), "orr": (3,3), "push": (4,1), "pop":(5,1), "ret": (6,0)}
pc = 0 # указатель на следующую инструкцию
lr = 0 # адрес возврата из функции
with open("hello.s", "r", encoding="utf8") as source:
lines = source.readlines()
for i in range(0,len(lines)):
lines[i] = lines[i].split("//")[0] \
.replace(",", " ") \
.strip().rstrip("\n") \
.lower()
while " " in lines[i]: # заменяем несколько пробелов одним
lines[i] = lines[i].replace(" ", " ")
if(lines[i]) == "": continue # если получилась пустая строка, переходим к следующей строке
tokens = lines[i].split(" ") # разбиваем инструкцию на токены
# если токен заканчивается на двоеточие, то это метка
if(tokens[0][-1]==":"):
label = tokens[0][:-1]
if(label in sym_tab):
print("Метка", label, "уже существует")
break
sym_tab[label] = addr # добавляем метку в таблицу символов
if(len(tokens)==1): continue # если инструкция на следующей строке, переходим к ней
else: tokens = tokens[1:] # получаем токены инструкции
instructions.append(tokens) # добавляем инструкцию в список instructions
addr = addr + 1 # увеличиваем указатель инструкций
# функция логгирования состояния программы
def print_state(instruction):
print(f"pc:{pc}", end=" ")
print(f"{instruction:<16}", end=" ")
for reg in regs32:
rInd = regs32[reg]
print(f"{reg}:0x{r[rInd]:04x}", end=" ")
# выводим флаги
print("\n" + " "*23 + f"c: {c} n: {n} z: {z}")
print(" "*23 + "stack:", stack, "\tsp: ", sp)
# получаем адрес, на который указывает метка
def get_label_addr(token):
if (token in sym_tab): return sym_tab[token]
print("Не найдена метка", token)
return None
# получаем индекс регистра
def get_register_index(token, show_error):
if (token in regs32): return regs32[token]
if show_error: print("Некорректный регистр", token)
return None
# получаем значение регистра
def get_register(token, show_error):
rInd = get_register_index(token, show_error)
if (rInd != None): return r[rInd]
return None
# получаем литерал
def get_literal(token):
try:
result = 0
if (token[0:2]=="0x"): result = int(token[2:],16) # если 16-ричное число
elif (token[0:2]=="0b"): result = int(token[2:],2) # если двоичное число
else: result= int(token)
return result & 0xffffffff # нормализуем литерал до 32 разрядов
except ValueError:
print("Некорректный токен", token)
return None
# получаем операнд, который может быть регистром или литералом
def get_register_or_literal(token):
reg = get_register(token, False)
if (reg != None): return reg
return get_literal(token)
# получаем количество параметров
def get_opType(tokens):
if tokens[0] not in mnemonics: # проверяем корректность инструкции
print("Некорректная инструкция ", tokens[0])
return None
# получаем количество операндов для данной инструкции и ее тип
type, count = mnemonics[tokens[0]]
if count!= len(tokens[1:]): # проверяем количество операндов
print("Некорректное количество операндов для инструкции: ", tokens[0])
return None
return type
# цикл обработки инструкций
while True:
if pc >= len(instructions): break # если инструкции закончились, то выход из цикла
tokens = instructions[pc] # получаем текущую инструкцию для выполнения
pc = pc + 1 # увеличиваем указатель инструкций
type = get_opType(tokens) # получаем количество операндов
if(type == None): break
# получаем операнды
op1, op2, op3 = 0, 0, 0
# если 1-й операнд - меткой
if(type==1): op1 = get_label_addr(tokens[1])
# если 1-й операнд - регистр
if(type in [2, 3, 5]): op1=get_register_index(tokens[1], True)
# если 1-й операнд - регистр или литерал (push)
if(type==4): op1 = get_register_or_literal(tokens[1])
# если 2-й операнд - регистр или литерал
if(type==2): op2 = get_register_or_literal(tokens[2])
# если 2-й операнд - регистр
# а 3-й операнд - регистр или литерал
if(type==3):
op2 = get_register(tokens[2], True)
op3 = get_register_or_literal(tokens[3])
# если какой-то параметр не установлен, завершаем цикл
if(None in [op1, op2, op3]): break
result = 0
match tokens[0]:
case "mov":
result = op2
case "and":
result = op2 & op3
case "orr":
result = op2 | op3
case "add":
result = op2 + op3
# если сумма больше 32 разрядов, устанавливаем флаг переноса
if(result > 0xffffffff): c = 1
else: c=0
case "sub":
result = op2 - op3
if(op2 < op3): c = 1 # если идет заимствование, устанавливаем флаг переноса
else: c=0
case "cmp":
result = r[op1] - op2
if(r[op1] < op2): c = 1 # если идет заимствование, устанавливаем флаг переноса
else: c=0
case "b": # если безусловный переход
pc = op1 # адрес следующей инструкции берем из op1
case "beq": # условный переход
if z==1: pc = op1 # если операнды равны
case "bne": # условный переход
if z==0: pc = op1 # если операнды НЕ равны
case "bl": # вызов функции
lr = pc # сохраняем адрес возврата в lr
pc = op1 # передаем адрес функции
case "ret": # выход из функции
pc = lr # передаем адрес следующей инструкции после вызова функции
case "push": # добавляем в стек
if sp==0:
print("Переполнение стека")
break
sp = sp - 1 # уменьшаем указатель стека. стек указывает на следующее свободное место
stack[sp] = op1 # помещаем в стек данные
case "pop": # получаем из стека
if(sp >= len(stack)):
print("Нельзя получить данные из пустого стека") # если стек пуст
break
result = stack[sp] # получаем данные
sp = sp + 1 # увеличиваем адрес в стеке
result = result & 0xffffffff # нормализация значения до 32 разрядов
# установка флагов
if(tokens[0] not in ["mov", "b", "beq", "bne", "bl", "pop", "push", "ret"]):
# получаем флаг знака
n = (result >> 31)
# получаем флаг нуля
z = 1 if result == 0 else 0
# установка целевого регистра
if(tokens[0] not in ["cmp", "b", "beq", "bne", "bl", "push", "ret"]):
r[op1] = result
print_state(" ".join(tokens)) # логгируем состояние программы на консоль
В отличие от предыдущих статей здесь добавлен условный регистр LR, который будет хранить адрес возврата из функции:
lr = 0 # адрес возврата из функции
В словарь мнемоник инструкций добавлены инструкции "bl" и "ret":
mnemonics = {"b": (1,1), "beq":(1,1), "bne":(1,1), "bl":(1,1), "mov":(2,2), "cmp":(2,2), "add": (3,3),
"sub":(3, 3), "and": (3,3), "orr": (3,3), "push": (4,1), "pop":(5,1), "ret": (6,0)}
Причем инструкция "ret" будет представлять 6-ю группу, которая не принимает никаких операндов.
При выполнении инструкции BL адрес следующей инструкции копируем в регистр LR, а в указатель PC передаем адрес функции:
case "bl": # вызов функции
lr = pc # сохраняем адрес возврата в lr
pc = op1 # передаем адрес функции
Таким образом, в новой итерации цикла начнет выплолняться функция
При выполнении инструкции RET адрес следующей инструкции копируем из регистра LR в указатель PC:
case "ret": # выход из функции
pc = lr # передаем адрес следующей инструкции после вызова функции
Таким образом, в новой итерации цикла начнет выплолняться код, который идет сразу за вызовом функции.
Протестируем функции и для этого в файле "hello.s" определим следующий код:
b _start // переходим к началу программы
// функция double - удваивает значение регистра r0
double:
add r0, r0, r0 // r0 = r0 + r0
ret
_start:
mov r0, 1
bl double // вызываем функцию double
Здесь в начале переходим к метке _start, за которой идут собственно инструкции программы. В данном случае помещаем в регистр r0 число 0. Затем вызываем функцию double, которая определена выше. В этой функции просто удваиваем значение регистра r0.
И при выполнении этой программы мы получим следующий вывод:
pc:3 b _start r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:4 mov r0 1 r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:1 bl double r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:2 add r0 r0 r0 r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:5 ret r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
Здесь мы видим, что после выполнения функции double значение в регистре r0 уведичилось в два раза.
Преимуществом функций является то, что мы можем их вызвать многократно в программе. Так, изменим файл "hello.s" следующим образом:
b _start // переходим к началу программы
// функция double - удваивает значение регистра r0
double:
add r0, r0, r0 // r0 = r0 + r0
ret
_start:
mov r0, 1
bl double
bl double
bl double
Здесь три раза вызываем функцию double. В итоге мы получим следующий вывод на консоль:
pc:3 b _start r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:4 mov r0 1 r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:1 bl double r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:2 add r0 r0 r0 r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:5 ret r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:1 bl double r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:2 add r0 r0 r0 r0:0x0004 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:6 ret r0:0x0004 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:1 bl double r0:0x0004 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:2 add r0 r0 r0 r0:0x0008 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
pc:7 ret r0:0x0008 r1:0x0000 r2:0x0000 r3:0x0000
c: 0 n: 0 z: 0
stack: [0, 0, 0, 0, 0, 0, 0, 0] sp: 8
Здесь мы видим, что значение регистра r0 3 раза удвоилось: 1 -> 2 -> 4 -> 8.
Аналогичным образом мы могли бы вызвать функцию в цикле:
b _start // переходим к началу программы
// функция double - удваивает значение регистра r0
double:
add r0, r0, r0 // r0 = r0 + r0
ret
_start:
mov r0, 1
mov r1, 0 // счетчик
while:
bl double
add r1, r1, 1
if:
cmp r1, 3
bne while