Функции

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

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

В ассемблере 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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850