Функции позволяют организовать код в отдельные независимые единицы, которые при необходимости можно многократно вызывать в течение работы программы. По сути функция представляет некоторые кусок кода, который проецируется на некоторую метку. В зависимости от архитектуры принцип работы функций может немного отличаться.
В ассемблере 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