Условные и безусловные переходы

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

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

Для управления переходами в различных ассемблерах применяются различные команды. Например, на Intel это команда JMP, тогда как в ARM это команда B. В качестве операнда они принимают адрес, к которому надо перейти. В качестве адреса обычно выступает метка:

jmp метка   // Intel
b метка     // ARM

Intel также позволяет переходить к адресу в регистре, а в ARM для этой цели есть отдельная инструкция - BR, которая принимает регистр и переходит к адресу в этом регистре.

jmp регистр  // Intel
br регистр     // ARM

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

CMP operand1, operand2

В реальности она производит вычитание operand1 - operand2. В этом плане она похожа на стандартную инструкцию вычитания с установкой флагов - SUB (Intel) и SUBS (ARM). Отличие состоит в том, что инструкция CMP не сохраняет результат вычитания в первый операнд, поскольку цель инструкции CMP состоит лишь в том, чтобы установить флаги кода условия на основе результата вычитания.

Далее с помощью флагов мы можем проверить условие. Например, если два операнда равны, то устанавливается флаг нуля (поскольку при вычитании двух равных чисел в результате получается 0). Аналогично если флаг нуля равен 0, то числа не равны. Используя другие флаги - знака, переноса и переполнения можно проверить более комплексные условия.

Для выполнения перехода в зависимости от условия различные архитектуры могут предоставлять широкий диапазон инструкций. Приведу только некоторые для:

IntelARM
jcbcs/b.csвыполняет переход, если флаг переноса установлен
jncbcc/b.ccвыполняет переход, если флаг переноса сброшен
jobcs/b.csвыполняет переход, если флаг переполнения установлен
jnobcc/b.ccвыполняет переход, если флаг переполнения сброшен
jsbmi/b.miвыполняет переход, если флаг знака установлен
jnsbpl/b.plвыполняет переход, если флаг знака сброшен
jz/jebeq/b.eqвыполняет переход, если флаг нуля установлен
jnz/jnebne/b.neвыполняет переход, если флаг нуля сброшен
jae/jnbbhs/b.hsвыполняет переход, если первый операнд больше или равен второму (беззнаковое сравнение)
jb/jnaeblo/b.loвыполняет переход, если первый операнд меньше второго (беззнаковое сравнение)
ja/jnbebhi/b.hiвыполняет переход, если первый операнд больше второго (беззнаковое сравнение)
jbe/jnabls/b.lsвыполняет переход, если первый операнд меньше или равен второму (беззнаковое сравнение)
jge/jnlbge/b.geвыполняет переход, если первый операнд больше или равен второму (сравнение со знаком)
jl/jngeblt/b.ltвыполняет переход, если первый операнд меньше второго (сравнение со знаком)
jg/jnlebgt/b.gtвыполняет переход, если первый операнд больше второго (сравнение со знаком)
jle/jngble/b.leвыполняет переход, если первый операнд меньше или равен второму (сравнение со знаком)

Рассмотрим реализацию переходов на примере трех инструкций, которые будут называться как и соответствующие инструкции ARM

  • B: инструкция безусловного перехода

  • BEQ: инструкция перехода, если два операнда равны (соответственно если флаг нуля установлен)

  • BNE: инструкция перехода, если два операнда не равны (соответственно если флаг нуля сброшен)

В качестве операнда все три инструкции будут принимать метку, к которой надо перейти:

B метка
BNE метка
BEQ метка

Для сравнения двух операндов будет использовать инструкцию CMP, которая принимает два сравниваемых операнда:

CMP operand1, operand2

Итак, определим следующую программу на языке Python:

instructions = []       # список инструкций
r = [0]*4       # значения 4 регистров
# карта сопоставления регистров и их индексов в списке r    
regs32 = {"r0":0, "r1":1, "r2":2, "r3":3}
pc = 0  # указатель на следующую инструкцию
# флаги 
c = 0   # флаг переноса
n = 0   # флаг знака
z = 0   # флан нуля

addr = 0        # адрес инструкции
sym_tab = {}      # таблица символов

# поддерживаемые инструкции и их типы
# 1 - инструкции с 1 операндом - меткой
# 2 - инструкции с 2 операндами, где первый операнд - регистр, а второй - регистр или литерал
# 3 - инструкции с 3 операндами, где первый и второй операнды - регистр, а третий - регистр или литерал
mnemonics = {"b": 1, "beq":1, "bne":1, "mov":2, "cmp":2, "add": 3, "sub":3, "and": 3, "orr": 3}

# считываем файл hello.s в список инструкций
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="   ")        # выводим значение PC
    print(f"{instruction:<18}", end=" ")     # выводим текущую инструкцию
    for reg in regs32:                      # выводим регистры
        rInd = regs32[reg]
        print(f"{reg}:0x{r[rInd]:04x}", end="  ")
    # выводим флаги
    print("\n" + " "*26 + f"c: {c}   n: {n}   z: {z}")

# получаем адрес, на который указывает метка
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_opCount(tokens):
    if tokens[0] not in mnemonics:          # проверяем корректность инструкции
        print("Некорректная инструкция ", " ".join(tokens))
        return None 
    # получаем количество операндов для данной инструкции
    count = mnemonics[tokens[0]] 
    if count!= len(tokens[1:]):     # проверяем количество операндов
        print("Некорректное количество операндов для инструкции: ", " ".join(tokens))
        return None
    return count

# цикл обработки инструкций
while True:
    if pc >= len(instructions): break  # если инструкции закончились, то выход из цикла
    tokens = instructions[pc]     # получаем текущую инструкцию для выполнения
    pc = pc + 1                 # увеличиваем указатель инструкций

    opCount = get_opCount(tokens)   # получаем количество операндов
    if(opCount == None): break

    # получаем операнды
    op1, op2, op3 = 0, 0, 0
    
    # если инструкция с 1-м операндом, то это метка
    if(opCount==1): op1 = get_label_addr(tokens[1])
    
    # если первый операнд - регистр
    if(opCount==2 or opCount==3): op1=get_register_index(tokens[1], True)
    
    # если инструкция с 2-мя операндами, то второй операнд может быть регистром или литералом
    if(opCount==2): op2 = get_register_or_literal(tokens[2])
        
    # если инструкция с 3-мя операндами, то второй операнд может быть регистром
    # а третий операнд может быть регистром или литералом
    if(opCount==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  # адрес следующей инструкции берем из 1-го операнда
        case "beq":       # условный переход
            if z==1: pc = op1  # если операнды равны
        case "bne":       # условный переход
            if z==0: pc = op1  # если операнды НЕ равны
            
    result = result & 0xffffffff
    if(tokens[0] not in ["mov", "b", "beq", "bne"]):
        # получаем флаг знака
        n = (result  >> 31)
        # получаем флаг нуля
        z = 1 if result == 0 else 0
        
    if(tokens[0] not in ["cmp", "b", "beq", "bne"]): r[op1] = result
    
    print_state(" ".join(tokens))    # логгируем состояние программы на консоль

Большая часть программы рассматривалась в предыдущих темах. Отмечу новые моменты. Прежде всего в словаре мнемоник инструкций определеные инструкции перехода, которые принимают по одному операнду - метку перехода:

mnemonics = {"b": 1, "beq":1, "bne":1, "mov":2, "cmp":2, "add": 3, "sub":3, "and": 3, "orr": 3}

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

def get_label_addr(token):
    if (token in sym_tab): return sym_tab[token]
    print("Не найдена метка", token)
    return None

Если метка не найдена возвращаем None и выводим сообщение об ошибке. Если метка найдена, возвращаем ее адрес.

В цикле обработки инструкций, если инструкция принимает только один операнд, считываем метку в переменную op1:

if(opCount==1): op1 = get_label_addr(tokens[1])

При сравнении с помощью инструкции CMP только устанавливаем результат:

case "cmp": 
    result = r[op1] - op2
    if(r[op1] < op2): c = 1 # если идет заимствование, устанавливаем флаг переноса
    else: c=0

При обработке инструкций перехода просто присваиваем адрес метки переменной pc, которая хранит адрес следующей выполняемой инструкции:

case "b":       # если безусловный переход
    pc = op1  # адрес следующей инструкции берем из op1
case "beq":       # условный переход
    if z==1: pc = op1  # если операнды равны
case "bne":       # условный переход
    if z==0: pc = op1  # если операнды НЕ равны

В конце устанавливаем остальные два флага:

if(tokens[0] not in ["mov", "b", "beq", "bne"]):
    # получаем флаг знака
    n = (result  >> 31)
    # получаем флаг нуля
    z = 1 if result == 0 else 0

И если инструкция не представляет инструкцию перехода или CMP, то изменяем целевой регистр:

if(tokens[0] not in ["cmp", "b", "beq", "bne"]): r[op1] = result

Для тестирования безусловных переходов определим следующий файл "hello.s":

    mov r0, 1
    b exit       // переход к метке exit
    mov r1, 2 
exit:           // метка exit
    mov r2, 3     

На второй строке идет переход к метке exit и соответственно к последней строке. Поэтому инструкция на 3-й строке не выполняется. Запустим программу:

pc:1  mov r0 1           r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 0
pc:3  b exit             r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 0
pc:4  mov r2 3           r0:0x0001 r1:0x0000 r2:0x0003 r3:0x0000 
                         c: 0   n: 0   z: 0

Здесь мы видим, что после выполнения второй строки, переменная pc получает значение 3 - условный адрес последней инструкции на 4-й строке. А 3-я строка с установкой регистра r1 не выполняется.

Теперь также проверим условные переходы. Для этого в "hello.s" определим следующую программу:

    mov r0, 2
    cmp r0, 2       // сравниваем r0 с числом 2
    beq equal      // переход к метке equal
    mov r1, 4       // если r0 != 2, то r1 = 4
    b end          // переход к концу программы
equal:           // метка equal
    mov r2, 8    // если r0 == 2, то r2 = 8
end:            // метка представляет конец программы

Здесь на второй строке сравниваем значение регистра r0 и число 2

cmp r0, 2

Если они равны, то с помощью инструкции beq переходим к метке equal

beq equal

На метке equal в регистр r2 помещаем число 8

mov r2, 8

Если числа не равны, то в регистр r1 помещаем число 4 и с помощью инструкции безусловного перехода переходим к концу программы - к метке end:

mov r1, 4       // если r0 != 2, то r1 = 4
b end          // переход к концу программы

При обработке этого файла мы получим следующий вывод:

pc:1  mov r0 2           r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 0
pc:2  cmp r0 2           r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 1
pc:5  beq equal          r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 1
pc:6  mov r2 8           r0:0x0002 r1:0x0000 r2:0x0008 r3:0x0000 
                         c: 0   n: 0   z: 1

Здесь мы видим, что после выполнения инструкции CMP устанавливается флаг нуля (флаг z). При выполнении инструкции условного перехода beq equal переменная pc получает адрес метки equal, поскольку флаг нуля установлен. Затем идет переход по этому адресу, и в регистр r2 помещается число 8.

Циклические конструкции

Теперь протестируем более сложную конструкцию - цикл на подобие while. Для этого в файле "hello.s" определим следующую программу:

    mov r0, 0
    b if            // переходим к проверке условия
while:
    add r0, r0, 1       // действия цикла - увеличиваем r0 на 1
if:                 // метка if - проверка условия цикла
    cmp r0, 3       // пока r0 не будет равен 3
    bne while       // если r0 !=3 , то повторяем цикл

Сначала в регистр r0 помещаем число 3. Данный цикл будет выступать в качестве счетчика цикла. С помощью инструкции b if сразу переходит к проверке условия в метке if.

На метке if с помощью инструкции CMP сравниваем значение регистра r0 с числом 3:

if:                 // метка if - проверка условия цикла
    cmp r0, 3       // пока r0 не будет равен 3

То есть мы будем выполнять цикл, пока регистр r0 не станет равен 3.

С помощью инструкции bne проверяем регистр r0 на неравенство 3:

bne while       // если r0 !=3, то повторяем цикл

Если r0 не равен 3, то переходим к метке while, после которой до метки if идут операции цикла. В данном случае это увеличение регистра r0 на 1

while:
    add r0, r0, 1       // действия цикла - увеличиваем r0 на 1

Таким образом, подобная программа будет аналогична следующей программе на Python:

r0 = 0
while r0 !=3: r0 = r0 + 1

И если мы запустим наш цикл на ассемблере, то получим следующий вывод:

pc:1  mov r0 0           r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 0
pc:3  b if               r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 0
pc:4  cmp r0 3           r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 1   n: 1   z: 0
pc:2  bne while          r0:0x0000 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 1   n: 1   z: 0
pc:3  add r0 r0 1        r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 0
pc:4  cmp r0 3           r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 1   n: 1   z: 0
pc:2  bne while          r0:0x0001 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 1   n: 1   z: 0
pc:3  add r0 r0 1        r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 0
pc:4  cmp r0 3           r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 1   n: 1   z: 0
pc:2  bne while          r0:0x0002 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 1   n: 1   z: 0
pc:3  add r0 r0 1        r0:0x0003 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 0
pc:4  cmp r0 3           r0:0x0003 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 1
pc:5  bne while          r0:0x0003 r1:0x0000 r2:0x0000 r3:0x0000 
                         c: 0   n: 0   z: 1

Таким образом, цикл будет выполняться 3 раза.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850